feat(workflow): add superpowers planning and execution state tracking

This commit is contained in:
csh 2026-05-18 16:28:06 +08:00
parent c1702a667a
commit 234b335663
11 changed files with 1287 additions and 310 deletions

View File

@ -40,8 +40,8 @@ Playbook工程规范与代理规则合集当前覆盖
`templates/` 目录除了语言配置模板外,还包含 AI 代理工作环境的项目架构模板:
- `templates/memory-bank/`项目上下文文档模板project-brief、tech-stack、architecture、progress、decisions
- `templates/prompts/`:工作流程模板agent-behavior、clarify、review
- `templates/memory-bank/`项目上下文文档模板project-brief、tech-context、system-patterns、active-context、progress、decisions
- `templates/prompts/`:工作流入口模板agent-behavior、clarify、verify-change、close-task、update-memory、code-review
- `templates/AGENTS.template.md`:路由中心模板(项目主入口)
- `templates/AGENT_RULES.template.md`:执行流程模板
@ -75,6 +75,26 @@ project_name = "MyProject"
- **CLAUDE.md**:自动检测(根目录 → `.claude/`),不存在则创建;注入 `@AGENTS.md` / `@AGENT_RULES.md`
- **force**:默认 false已存在则跳过设为 true 时强制覆盖(会先备份)
### 工作流留痕 helper
如果项目已经部署了这套模板,并使用 `superpowers` 工作流:
```bash
# spec 写完后
python <deploy_root>/scripts/playbook.py \
-record-spec docs/superpowers/specs/<topic>-design.md \
-progress memory-bank/progress.md
# plan 写完后
python <deploy_root>/scripts/playbook.py \
-record-plan docs/superpowers/plans/<topic>.md \
-progress memory-bank/progress.md
```
这两个 helper 只负责把 `workflow-state` 写入
`memory-bank/progress.md`
真正执行 Plan 仍然走 `main_loop.py claim/finish`
详见:`templates/README.md`
## rulesets/(规则集模板库 - 三层架构)
@ -159,11 +179,11 @@ TSL 相关问题直接查阅 `rulesets/tsl/index.md` 与 `docs/tsl/`。
### 快速决策:我应该用哪种方式?
| 你的情况 | 推荐方式 | 优势 |
| --- | --- | --- |
| 新项目,需要持续同步更新 | 方式一:`git subtree` | 标准留在项目内,后续可拉取更新 |
| 不想把 Playbook 以 subtree 嵌进仓库,但仍要把标准部署到项目内 | 方式二:外部 clone 后执行部署 | Playbook 仓库与业务仓库解耦,部署根目录可配置 |
| **不确定?** | **方式一:`git subtree`(推荐)** | 项目内可见、版本可追溯、使用路径最稳定 |
| 你的情况 | 推荐方式 | 优势 |
| ------------------------------------------------------------- | --------------------------------- | --------------------------------------------- |
| 新项目,需要持续同步更新 | 方式一:`git subtree` | 标准留在项目内,后续可拉取更新 |
| 不想把 Playbook 以 subtree 嵌进仓库,但仍要把标准部署到项目内 | 方式二:外部 clone 后执行部署 | Playbook 仓库与业务仓库解耦,部署根目录可配置 |
| **不确定?** | **方式一:`git subtree`(推荐)** | 项目内可见、版本可追溯、使用路径最稳定 |
---
@ -251,7 +271,6 @@ git commit -m ":package: deps(playbook): add tsl standards"
```
2. 在目标项目根创建 `playbook.toml`,并用 `deploy_root` 指定项目内的部署根。例如:
- `project_root` 写目标项目根目录。
- `deploy_root` 写目标项目内的相对路径。
- 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `deploy_root`;那只是你执行脚本的位置。

View File

@ -1,12 +1,28 @@
#!/usr/bin/env python3
from contextlib import contextmanager
import os
import platform
import re
import sys
import threading
import time
from pathlib import Path
from typing import Optional
try:
import fcntl
except ImportError: # pragma: no cover
fcntl = None
try:
import msvcrt
except ImportError: # pragma: no cover
msvcrt = None
PLAN_STATUS_START = "<!-- plan-status:start -->"
PLAN_STATUS_END = "<!-- plan-status:end -->"
WORKFLOW_STATE_START = "<!-- workflow-state:start -->"
WORKFLOW_STATE_END = "<!-- workflow-state:end -->"
PLAN_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$")
PLAN_LINE_RE = re.compile(
r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` "
@ -15,6 +31,9 @@ PLAN_LINE_RE = re.compile(
)
FINISH_STATUSES = {"done", "blocked", "skipped"}
ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$")
WORKFLOW_PHASES = {"brainstorming", "planning", "executing", "done", "blocked"}
THREAD_LOCKS: dict[str, threading.Lock] = {}
THREAD_LOCKS_GUARD = threading.Lock()
def usage() -> str:
@ -23,12 +42,19 @@ def usage() -> str:
" python scripts/main_loop.py claim -plans <dir> -progress <file>\n"
" python scripts/main_loop.py finish -plan <path> -status <status> "
"-progress <file> [-note <text>]\n"
" python scripts/main_loop.py record -progress <file> -phase <phase> "
"[-spec <path>] [-plan <path>] [-executor <name>] "
"[-constraints <csv>]\n"
" python scripts/main_loop.py -h\n"
"Options:\n"
" -plans DIR\n"
" -plan PATH\n"
" -status done|blocked|skipped\n"
" -progress FILE\n"
" -phase brainstorming|planning|executing|done|blocked\n"
" -spec PATH\n"
" -executor NAME\n"
" -constraints CSV\n"
" -note TEXT\n"
" -h, -help Show this help.\n"
)
@ -53,9 +79,9 @@ def parse_flags(args: list[str]) -> dict[str, str]:
def normalize_plan_key(plan_value: str) -> str:
raw = plan_value.strip().replace("\\", "/")
raw = raw.lstrip("./")
if raw.startswith("docs/plans/"):
return raw[len("docs/plans/") :]
marker = "/docs/plans/"
if raw.startswith("docs/superpowers/plans/"):
return raw[len("docs/superpowers/plans/") :]
marker = "/docs/superpowers/plans/"
if marker in raw:
return raw.split(marker, 1)[1]
return raw
@ -98,6 +124,22 @@ def find_block(lines: list[str]) -> Optional[tuple[int, int]]:
return None
def find_named_block(
lines: list[str], start_marker: str, end_marker: str
) -> Optional[tuple[int, int]]:
start_idx = None
for idx, line in enumerate(lines):
if line.strip() == start_marker:
start_idx = idx
break
if start_idx is None:
return None
for idx in range(start_idx + 1, len(lines)):
if lines[idx].strip() == end_marker:
return start_idx, idx
return None
def parse_entries(
lines: list[str], start_idx: int, end_idx: int
) -> list[tuple[str, str, Optional[str], int]]:
@ -115,13 +157,60 @@ def parse_entries(
def render_progress_lines(plans: list[str]) -> list[str]:
lines = ["# Plan 状态", "", PLAN_STATUS_START]
lines = [
"# 当前进展",
"",
"## Workflow State",
"",
WORKFLOW_STATE_START,
WORKFLOW_STATE_END,
"",
"## Plan Status",
"",
PLAN_STATUS_START,
]
for plan_key in plans:
lines.append(render_plan_line(plan_key, "pending", None))
lines.append(PLAN_STATUS_END)
return lines
def render_workflow_state_lines(
phase: Optional[str] = None,
spec: Optional[str] = None,
plan: Optional[str] = None,
executor: Optional[str] = None,
constraints: Optional[str] = None,
) -> list[str]:
lines = [WORKFLOW_STATE_START]
if phase:
lines.append(f"phase: {phase}")
if spec:
lines.append(f"spec: {spec}")
if plan:
lines.append(f"plan: {plan}")
if executor:
lines.append(f"executor: {executor}")
if constraints:
lines.append(f"constraints: {constraints}")
lines.append(WORKFLOW_STATE_END)
return lines
def parse_workflow_state(
lines: list[str], start_idx: int, end_idx: int
) -> dict[str, str]:
state: dict[str, str] = {}
for idx in range(start_idx + 1, end_idx):
line = lines[idx].strip()
if ": " not in line:
continue
key, value = line.split(": ", 1)
if key in {"phase", "spec", "plan", "executor", "constraints"}:
state[key] = value
return state
def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
if not note:
return None
@ -147,17 +236,132 @@ def write_progress_lines(progress_path: Path, lines: list[str]) -> None:
progress_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def get_thread_lock(lock_path: Path) -> threading.Lock:
key = str(lock_path.resolve())
with THREAD_LOCKS_GUARD:
lock = THREAD_LOCKS.get(key)
if lock is None:
lock = threading.Lock()
THREAD_LOCKS[key] = lock
return lock
@contextmanager
def locked_progress(progress_path: Path):
progress_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = progress_path.with_name(f"{progress_path.name}.lock")
thread_lock = get_thread_lock(lock_path)
with thread_lock:
with lock_path.open("a+b") as lock_file:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
elif msvcrt is not None: # pragma: no cover
while True:
try:
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
break
except OSError:
time.sleep(0.05)
try:
hold_ms = os.environ.get("PLAYBOOK_MAIN_LOOP_HOLD_LOCK_MS")
if hold_ms:
time.sleep(max(0.0, float(hold_ms) / 1000.0))
yield
finally:
if fcntl is not None:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
elif msvcrt is not None: # pragma: no cover
lock_file.seek(0)
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
def ensure_section(lines: list[str], heading: str) -> list[str]:
if any(line.strip() == heading for line in lines):
return lines
if lines and lines[-1] != "":
lines.append("")
lines.extend([heading, ""])
return lines
def ensure_block_with_lines(
lines: list[str],
start_marker: str,
end_marker: str,
default_lines: list[str],
heading: Optional[str] = None,
) -> tuple[list[str], int, int]:
block = find_named_block(lines, start_marker, end_marker)
if block:
return lines, block[0], block[1]
if not lines:
lines = ["# 当前进展", ""]
if heading:
lines = ensure_section(lines, heading)
if lines and lines[-1] != "":
lines.append("")
insert_at = len(lines)
lines[insert_at:insert_at] = default_lines
return lines, insert_at, insert_at + len(default_lines) - 1
def ensure_plan_block(
lines: list[str], progress_path: Path, plan_keys: list[str]
) -> tuple[list[str], int, int]:
block = find_block(lines)
if not block:
lines = render_progress_lines(plan_keys)
write_progress_lines(progress_path, lines)
block = find_block(lines)
if not block:
raise ValueError("failed to create plan status block")
return lines, block[0], block[1]
lines, _, _ = ensure_workflow_state_block(lines)
lines, start_idx, end_idx = ensure_block_with_lines(
lines,
PLAN_STATUS_START,
PLAN_STATUS_END,
[PLAN_STATUS_START, PLAN_STATUS_END],
"## Plan Status",
)
write_progress_lines(progress_path, lines)
return lines, start_idx, end_idx
def ensure_workflow_state_block(
lines: list[str],
) -> tuple[list[str], int, int]:
return ensure_block_with_lines(
lines,
WORKFLOW_STATE_START,
WORKFLOW_STATE_END,
[WORKFLOW_STATE_START, WORKFLOW_STATE_END],
"## Workflow State",
)
def update_workflow_state(
lines: list[str],
phase: Optional[str] = None,
spec: Optional[str] = None,
plan: Optional[str] = None,
executor: Optional[str] = None,
constraints: Optional[str] = None,
) -> list[str]:
lines, start_idx, end_idx = ensure_workflow_state_block(lines)
state = parse_workflow_state(lines, start_idx, end_idx)
if phase is not None:
state["phase"] = phase
if spec is not None:
state["spec"] = spec
if plan is not None:
state["plan"] = plan
if executor is not None:
state["executor"] = executor
if constraints is not None:
state["constraints"] = constraints
lines[start_idx : end_idx + 1] = render_workflow_state_lines(
state.get("phase"),
state.get("spec"),
state.get("plan"),
state.get("executor"),
state.get("constraints"),
)
return lines
def ensure_all_plans_present(
@ -175,6 +379,13 @@ def ensure_all_plans_present(
return entries
def filter_existing_entries(
entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str]
) -> list[tuple[str, str, Optional[str], int]]:
available = set(plan_keys)
return [entry for entry in entries if entry[0] in available]
def choose_claim_entry(
entries: list[tuple[str, str, Optional[str], int]], current_env: Optional[str]
) -> Optional[tuple[str, Optional[str], int]]:
@ -205,20 +416,27 @@ def claim_plan(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
if not plan_keys:
return 2, "ERROR: no plan files found"
lines = load_progress_lines(progress_path)
try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, plan_keys)
except ValueError as exc:
return 2, f"ERROR: {exc}"
with locked_progress(progress_path):
lines = load_progress_lines(progress_path)
try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, plan_keys)
except ValueError as exc:
return 2, f"ERROR: {exc}"
entries = ensure_all_plans_present(lines, start_idx, end_idx, progress_path, plan_keys)
chosen = choose_claim_entry(entries, detect_env())
if not chosen:
return 2, "ERROR: no claimable plans"
entries = ensure_all_plans_present(lines, start_idx, end_idx, progress_path, plan_keys)
entries = filter_existing_entries(entries, plan_keys)
chosen = choose_claim_entry(entries, detect_env())
if not chosen:
return 0, "NOOP: no claimable plans"
plan_key, note, idx = chosen
lines[idx] = render_plan_line(plan_key, "in-progress", note)
write_progress_lines(progress_path, lines)
plan_key, note, idx = chosen
lines[idx] = render_plan_line(plan_key, "in-progress", note)
lines = update_workflow_state(
lines,
phase="executing",
plan=(plans_dir / plan_key).as_posix(),
)
write_progress_lines(progress_path, lines)
output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
if note:
@ -234,27 +452,58 @@ def finish_plan(
if not plan:
return 2, "ERROR: plan is required"
lines = load_progress_lines(progress_path)
plan_key = normalize_plan_key(plan)
with locked_progress(progress_path):
lines = load_progress_lines(progress_path)
try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, [plan_key])
except ValueError as exc:
return 2, f"ERROR: {exc}"
try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, [plan_key])
except ValueError as exc:
return 2, f"ERROR: {exc}"
entries = parse_entries(lines, start_idx, end_idx)
rendered_note = normalize_note(note) if note else None
updated_line = render_plan_line(plan_key, status, rendered_note)
entries = parse_entries(lines, start_idx, end_idx)
rendered_note = normalize_note(note) if note else None
updated_line = render_plan_line(plan_key, status, rendered_note)
for entry_plan, _, _, idx in entries:
if entry_plan == plan_key:
lines[idx] = updated_line
write_progress_lines(progress_path, lines)
return 0, updated_line
for entry_plan, _, _, idx in entries:
if entry_plan == plan_key:
lines[idx] = updated_line
workflow_phase = "done" if status == "done" else "blocked"
lines = update_workflow_state(
lines,
phase=workflow_phase,
plan=f"docs/superpowers/plans/{plan_key}",
)
write_progress_lines(progress_path, lines)
return 0, updated_line
lines[end_idx:end_idx] = [updated_line]
write_progress_lines(progress_path, lines)
return 0, updated_line
lines[end_idx:end_idx] = [updated_line]
workflow_phase = "done" if status == "done" else "blocked"
lines = update_workflow_state(
lines,
phase=workflow_phase,
plan=f"docs/superpowers/plans/{plan_key}",
)
write_progress_lines(progress_path, lines)
return 0, updated_line
def record_workflow_state(
progress_path: Path,
phase: str,
spec: Optional[str],
plan: Optional[str],
executor: Optional[str],
constraints: Optional[str],
) -> tuple[int, str]:
if phase not in WORKFLOW_PHASES:
return 2, f"ERROR: invalid phase: {phase}"
with locked_progress(progress_path):
lines = load_progress_lines(progress_path)
lines = update_workflow_state(lines, phase, spec, plan, executor, constraints)
write_progress_lines(progress_path, lines)
return 0, "OK"
def main(argv: list[str]) -> int:
@ -266,7 +515,7 @@ def main(argv: list[str]) -> int:
return 0
mode = argv[0]
if mode not in {"claim", "finish"}:
if mode not in {"claim", "finish", "record"}:
print(f"ERROR: unknown mode: {mode}", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
@ -295,6 +544,26 @@ def main(argv: list[str]) -> int:
print(message)
return 0
if mode == "record":
progress = flags.get("-progress")
phase = flags.get("-phase")
spec = flags.get("-spec")
plan = flags.get("-plan")
executor = flags.get("-executor")
constraints = flags.get("-constraints")
if not progress or not phase:
print("ERROR: -progress and -phase are required", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
code, message = record_workflow_state(
Path(progress), phase, spec, plan, executor, constraints
)
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")

View File

@ -5,6 +5,7 @@ from datetime import datetime, timezone
from pathlib import Path
from shutil import copy2, copytree, rmtree, which
import subprocess
import importlib.util
try:
import tomllib
@ -22,6 +23,11 @@ ORDER = [
]
SCRIPT_DIR = Path(__file__).resolve().parent
PLAYBOOK_ROOT = SCRIPT_DIR.parent
MAIN_LOOP_SCRIPT = SCRIPT_DIR / "main_loop.py"
MAIN_LOOP_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", MAIN_LOOP_SCRIPT)
assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC)
MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home", "skill_link"}
DOCS_INDEX_SECTION_HEADINGS = {
"common": "## 跨语言common",
@ -34,7 +40,23 @@ DOCS_INDEX_SECTION_HEADINGS = {
def usage() -> str:
return "Usage:\n python scripts/playbook.py -config <path>\n python scripts/playbook.py -h"
return (
"Usage:\n"
" python scripts/playbook.py -config <path>\n"
" python scripts/playbook.py -record-spec <spec_path> -progress <path>\n"
" python scripts/playbook.py -record-plan <plan_path> -progress <path>\n"
" python scripts/playbook.py -h"
)
def parse_cli_value(argv: list[str], flag: str) -> Optional[str]:
if flag not in argv:
return None
idx = argv.index(flag)
if idx + 1 >= len(argv):
return None
value = argv[idx + 1].strip()
return value or None
def strip_inline_comment(value: str) -> str:
@ -1393,6 +1415,47 @@ def main(argv: list[str]) -> int:
if "-h" in argv or "-help" in argv:
print(usage())
return 0
spec_path = parse_cli_value(argv, "-record-spec")
if spec_path is not None:
progress_path = parse_cli_value(argv, "-progress")
if not progress_path:
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
return 2
code, message = MAIN_LOOP.record_workflow_state(
Path(progress_path),
"planning",
spec_path,
None,
None,
None,
)
if code != 0:
print(message, file=sys.stderr)
return code
print(message)
return 0
plan_path = parse_cli_value(argv, "-record-plan")
if plan_path is not None:
progress_path = parse_cli_value(argv, "-progress")
if not progress_path:
print("ERROR: -progress is required.\n" + usage(), file=sys.stderr)
return 2
code, message = MAIN_LOOP.record_workflow_state(
Path(progress_path),
"planning",
None,
plan_path,
"executing-plans",
"karpathy-guidelines,.agents,AGENT_RULES",
)
if code != 0:
print(message, file=sys.stderr)
return code
print(message)
return 0
if "-config" not in argv:
print("ERROR: -config is required.\n" + usage(), file=sys.stderr)
return 2

View File

@ -6,10 +6,7 @@
<!-- playbook:agents:start -->
请以 `.agents/` 下的规则为准:
- 入口:`.agents/index.md`
- 语言规则:见 `.agents/index.md` 与对应语言子目录
- [.agents/index.md](.agents/index.md) - 语言规则与工具入口
<!-- playbook:agents:end -->
<!-- playbook:templates:start -->
@ -18,19 +15,15 @@
- [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/active-context.md](memory-bank/active-context.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/review.md](docs/prompts/coding/review.md) - 复盘总结
- [docs/prompts/system/agent-behavior.md](docs/prompts/system/agent-behavior.md) - 工作模式参考
- [docs/prompts/README.md](docs/prompts/README.md) - 提示词与流程入口
<!-- playbook:templates:end -->
<!-- playbook:framework:end -->

View File

@ -9,235 +9,278 @@
3. 仓库规则:`.agents/` 与 `AGENTS.md`
4. 本文件
## 安全红线
## 安全与沟通
- 不得在代码/日志/注释中写入明文密钥、密码、Token
- 修改鉴权/权限逻辑必须说明动机与风险
- 不确定是否敏感时按敏感信息处理
- 执行修改文件系统的命令前,必须解释目的和潜在影响
### 安全红线
## 行为准则
- 不得在代码、日志或注释中写入明文密钥、密码、Token
- 修改鉴权、权限或敏感数据流时,必须说明动机与风险
- 不确定是否敏感时,一律按敏感信息处理
- 执行会修改文件系统的命令前,必须说明目的与潜在影响
### 项目适应
### 沟通原则
- **模仿项目风格**:优先分析周围代码和配置,遵循现有约定
- **不假设可用性**:不假设库或框架可用,先验证再使用
- **完整完成请求**:不遗漏用户要求的任何部分
- 统一使用简体中文
- 专业、直接、简洁,避免对话填充词
- 发现用户理解有误时,礼貌纠正
- 无法满足请求时,简洁说明原因并提供替代方案
- 不给时间估算,专注事实、风险与下一步
- 代码块必须标注语言类型
- 不使用 emoji除非用户明确要求
### 技术态度
## 工作原则
- **准确性优先**:技术准确性优先于迎合用户
- **诚实纠正**:发现用户理解有误时,礼貌纠正
- **先查后答**:不确定时先调查再回答
- 模仿项目现有风格,先看周围代码、配置和测试再动手
- 不假设库、框架或命令可用,先验证再使用
- 完整覆盖用户请求,不遗漏边界条件和收尾工作
- 技术准确性优先于迎合;不确定时先调查再回答
- 只做当前任务需要的改动,不顺手加功能、不顺手重构
- 不为一次性操作增加抽象,不为假设的未来需求设计
### 避免过度工程
## 会话启动
- **只做要求的**:不主动添加未要求的功能或重构
- **不过度抽象**:不为一次性操作创建工具函数
- **不为未来设计**:不为假设的未来需求设计
每次新会话开始时,按顺序加载以下上下文:
## 沟通原则
1. `AGENT_RULES.local.md`:项目私有规则(如存在)
2. `.agents/index.md`:语言规则与工具入口(如存在)
3. `memory-bank/project-brief.md`:项目定位、边界、约束
4. `memory-bank/tech-context.md`:技术上下文、工具链、验证命令
5. `memory-bank/system-patterns.md`:系统模式、边界与不变量
6. `memory-bank/active-context.md`:当前目标、最近变更、下一步
7. `memory-bank/decisions.md`:重要决策记录(如存在)
8. `memory-bank/progress.md`:执行进度与 Plan 状态(如存在)
9. `docs/superpowers/specs/`:最新设计稿(如存在)
10. `docs/superpowers/plans/`:相关实施计划(如存在)
- **统一简体中文**:所有回复均使用简体中文
- **简洁直接**:专业、直接、简洁,避免对话填充词
- **拒绝时提供替代**:无法满足请求时,简洁说明并提供替代方案
- **不给时间估算**:专注任务本身,让用户自己判断时间
- **代码块标注语言**:输出代码时标注语言类型
- **不使用 emoji**:除非用户明确要求
目的:快速建立项目全貌,避免重复解释和重复试错。
## 上下文加载(每次会话开始)
## 规划与执行模型
**必读文档**(按顺序):
- 头脑风暴使用 `$brainstorming`,产出
`docs/superpowers/specs/*-design.md`
- 实施计划使用 `$writing-plans`,产出
`docs/superpowers/plans/*.md`
- Plan 生命周期由 `main_loop.py` 协调,并通过
`memory-bank/progress.md` 留痕
- 默认执行器是 `$executing-plans`
- 代码类执行必须同时遵循:
`karpathy-guidelines`、`.agents/`、`AGENT_RULES.md`
1. `AGENT_RULES.local.md` - 项目私有规则(如存在,优先级高于本文件)
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 快速理解项目全貌,避免重复解释。
- 规划阶段必须走 `using-superpowers -> brainstorming -> writing-plans`
- `brainstorming` 写出 spec 后,立即用 `playbook.py -record-spec`
记录 `phase=planning``spec=<path>`
- `writing-plans` 写出 plan 后,立即用 `playbook.py -record-plan`
记录 `plan=<path>`、`executor=executing-plans`、
`constraints=karpathy-guidelines,.agents,AGENT_RULES`
- 未领取 Plan 前,不得直接进入 `$executing-plans`
- 已领取 Plan 后,默认执行使用 `$executing-plans`
- `$subagent-driven-development` 仅在 Plan 或平台明确要求时使用,
不是默认执行器
- 执行完成后,必须先运行 `main_loop.py finish` 写回状态,
再更新 `progress.md` 上半部分摘要
## 规划与执行分工
### Plan 要求
| 阶段 | 工具 | 产出 | 留痕 |
| ------------ | ---------------------- | ----------------- | -------------------------- |
| 头脑风暴 | `$brainstorming` skill | 设计思路 | 无 |
| 生成计划 | `$writing-plans` skill | `docs/plans/*.md` | 无 |
| **执行计划** | **`main_loop.py` 主循环** | 代码/配置变更 | **`memory-bank/progress.md`** |
- `Plan Meta` 必填,位于 Plan 头部 `---` 之后、Task 1 之前
- `Plan Meta` 至少包含:
- `Plan Group`
- `Parent Plan`
- `Verification Scope`
- `Verification Gate`
- Plan 中不得包含必然失败或依赖未确认的信息
- 未确认项必须在 `$brainstorming` 阶段解决后,才能产出 Plan
- Plan 内验证必须是当前阶段可通过的局部验证
- 需要集成验证的内容,放入上层或集成 Plan
- Plan 生成完成后,执行入口只能是主循环
- 代码类 Plan 应显式声明执行约束:
`karpathy-guidelines`、`.agents/`、`AGENT_RULES.md`
- 不因等待确认而中断可执行步骤;待确认事项写入回复
- 每个 Plan 应小步、可验证、可快速完成
> **重要**:第三方 skills 只用于规划,不负责执行留痕。收到执行触发词后,不得直接使用 `$executing-plans`,也不得直接使用 `$subagent-driven-development`;必须先运行 `main_loop.py claim` 领取 Plan再通过 `main_loop.py finish` 写回结果。
## 主循环执行契约
## 主循环
### 触发方式
**触发词**
- 常规模式:`执行主循环`、`继续执行`、`下一个 Plan`
- 无交互模式:`自动执行所有 Plan`
| 触发词 | 模式 | 说明 |
| --------------------------------------- | ---------- | ---------------------- |
| `执行主循环`、`继续执行`、`下一个 Plan` | 常规模式 | 遇确认场景可询问用户 |
| `自动执行所有 Plan` | 无交互模式 | 不询问,按规则自动处理 |
### Plan 状态
**Plan 状态**
- `pending`:待执行
- `in-progress`:执行中,用于恢复中断任务
- `done`:已完成
- `blocked`:阻塞,需人工介入或切换环境
- `skipped`:永久跳过,不再执行
| 状态 | 含义 |
| ----------- | ------------------------- |
| pending | 待执行 |
| in-progress | 执行中(崩溃恢复用) |
| done | 已完成 |
| blocked | 阻塞(需人工介入) |
| skipped | 跳过Plan 不再需要执行) |
`skipped` 如需恢复,必须手动改回 `pending`
> 说明:`skipped` 仅用于永久不再执行;如需恢复执行,需手动改回 `pending`
### 环境阻塞格式
**环境阻塞格式**`blocked: env:<环境>:<Task列表>`
- 格式:`env:<环境>:<Task列表>`
- 示例:`env:windows:Task2,Task4`
- `Task` 列表必须使用英文逗号分隔,且不要包含空格
- 示例:`blocked: env:windows:Task2,Task4`
- 含义:需要在指定环境执行列出的 Task
- 约束:`Task` 列表使用英文逗号分隔,不要包含空格,便于解析
### 领取与写回
**流程**
领取命令:
1. 领取 Plan
- 运行 `python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim -plans docs/plans -progress memory-bank/progress.md`
- 该命令会**原子化**完成三件事:
- 自动识别当前环境(`windows` / `linux` / `darwin`
- 选择第一个可执行的 Plan优先恢复 `in-progress`,其次 `pending`,最后 `blocked: env:<当前环境>:...`
- 将选中的 Plan 写成 `in-progress`
- stdout 必须包含 `PLAN=<path>`;如果是从环境阻塞恢复,还会附带 `NOTE=env:<环境>:<Task列表>`
- 如无可执行 Plan跳到步骤 6
2. 阅读领取结果:
- 记录 `PLAN=` 返回的路径
- 如果 stdout 含 `NOTE=env:...`,本轮只执行列出的 Task
3. 阅读 Plan
- 理解目标、子任务与验证标准
- **注意**Plan 文档中的 execution handoff / REQUIRED SUB-SKILL 仅作参考;如与本文件冲突,一律以主循环为准
4. 逐步执行:
- 按顺序执行 Task
- 每个 Task 完成后进行必要验证(测试/日志/diff
- **Task 失败处理**
- 环境不匹配(`command not found`、路径不存在)→ 记录该 Task 及所需环境,**继续下一个 Task**
- 其他阻塞 → 记录原因,跳到步骤 6 标记 Plan blocked
- **安全红线**(明文密钥等)→ 立即停止,不继续后续 Plan
- 遇到歧义/风险/决策点:
- 常规模式:记录到回复中,可询问用户
- 无交互模式:按「需要确认的场景」规则自动处理
5. 写回结果:
- 全部完成:
- `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan <plan> -status done -progress memory-bank/progress.md`
- 有 Task 因环境跳过:
- `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan <plan> -status blocked -progress memory-bank/progress.md -note "env:<所需环境>:<Task列表>"`
- 其他阻塞:
- `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan <plan> -status blocked -progress memory-bank/progress.md -note "<原因>"`
- 跳过整个 Plan
- `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan <plan> -status skipped -progress memory-bank/progress.md -note "<原因>"`
- 写回后回到步骤 1 继续下一个 Plan
6. 汇总报告(所有 Plan 处理完毕后):
- 已完成的 Plan
- 阻塞/跳过的 Plan 及原因
- 需要在其他环境执行的 Plan`blocked: env:...`
- 待确认的歧义/风险/决策点
- 如需记录重要决策,写入 `memory-bank/decisions.md`
7. **结束**:主循环终止
```bash
python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim \
-plans docs/superpowers/plans \
-progress memory-bank/progress.md
```
## Plan 规则
该命令会在锁保护下串行完成三件事:
- **Plan Meta 必填**Plan 头部 `---` 之后、Task 1 之前插入 `## Plan Meta`,包含:
- `Plan Group`(归类任务)
- `Parent Plan`(上层/集成计划链接)
- `Verification Scope`local 或 integration
- `Verification Gate`must-pass
- **不允许中断任务**Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 `$brainstorming` 阶段解决后再产出 Plan
- **验证必须可通过**Plan 内验证应为当前阶段可通过的局部验证;需要集成验证的内容放入上层/集成 Plan
- **执行入口唯一**Plan 生成完成后,后续执行只能由主循环驱动;不得按 Plan 头部说明直接切换到 `$executing-plans``$subagent-driven-development`
- 不因等待确认而中断可执行步骤;待确认事项在回复中列出
- 每轮只处理一个 Plan
- **小步快跑**:每个 Plan 应该可快速完成
- **可验证**:每个 Plan 必须包含验证步骤
- 自动识别当前环境:`windows`、`linux`、`darwin`
- 按顺序选择可执行 Plan
`in-progress` > `pending` > `blocked: env:<当前环境>:...`
- 将选中的 Plan 写成 `in-progress`
## 复利工程
这里的锁保护的是 `progress.md` 状态块更新,避免多个 session
同时读写时发生覆盖。
每次 Session 结束时
stdout 必须包含:
- **同一错误发生 2 次以上** → 立即更新 `AGENT_RULES.local.md``memory-bank/decisions.md`,避免下次重蹈
- **发现项目特有规律**(如特定模块的注意事项、常见陷阱)→ 沉淀到 `AGENT_RULES.local.md`
- `PLAN=<path>`
- 如为环境恢复,还会附带 `NOTE=env:<环境>:<Task列表>`
> 目标:让每次 Session 的起点比上次更高。
规划与执行留痕示例:
## 执行约束
```bash
# brainstorming 完成后
python {{PLAYBOOK_SCRIPTS}}/playbook.py \
-record-spec docs/superpowers/specs/<topic>-design.md \
-progress memory-bank/progress.md
```
### 代码修改
```bash
# writing-plans 完成后
python {{PLAYBOOK_SCRIPTS}}/playbook.py \
-record-plan docs/superpowers/plans/<topic>.md \
-progress memory-bank/progress.md
```
- **必须先读文件再修改**:不读文件就提议修改是禁止的
- **必须运行测试验证**:相关测试必须通过
- **遵循换行规则**:遵循 `.gitattributes` 规则
- **命名一致性**:遵循项目现有的命名风格
- **最小改动原则**:只修改必要的部分,不顺手重构
写回命令示例:
### 决策记录
```bash
python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
-plan <plan> \
-status done \
-progress memory-bank/progress.md
```
- **重要决策**:记录到 `memory-bank/decisions.md`ADR 格式)
- **待确认事项**:在回复中列出并等待确认
- **进度留痕**:通过 `{{PLAYBOOK_SCRIPTS}}/main_loop.py` 维护 `memory-bank/progress.md` 的 Plan 状态块(唯一权威)
```bash
python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
-plan <plan> \
-status blocked \
-progress memory-bank/progress.md \
-note "env:<所需环境>:<Task列表>"
```
```bash
python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
-plan <plan> \
-status blocked|skipped \
-progress memory-bank/progress.md \
-note "<原因>"
```
### 执行规则
1. 先 `claim`,拿到 `PLAN=` 后再读取 Plan 内容
2. 如返回 `NOTE=env:...`,本轮只执行列出的 Task
3. 默认执行器是 `$executing-plans`;代码类任务在执行前必须显式
加载 `karpathy-guidelines`
4. 执行时同时遵循 `.agents/`、`AGENT_RULES.md` 和 Plan 本身;
如发生冲突,以优先级更高的规则为准
5. 按顺序执行 Task并完成 Plan 约定的验证
6. 环境不匹配时,记录所需环境和 Task继续处理本 Plan
其余可执行 Task
7. 其他阻塞写回 `blocked`;永久放弃写回 `skipped`
8. 触碰安全红线时立即停止,不继续后续 Plan
9. 常规模式下可对高风险事项向用户确认;无交互模式按本文件
的“需要确认的场景”自动处理
10. 每次 `claim` 只领取一个 Plan写回后再领取下一个
11. 全部 Plan 处理完后,统一汇总完成项、阻塞项、跳过项、
环境需求与待确认事项
## 通用执行约束
### 代码与配置修改
- 必须先读文件再修改
- 遵循 `.agents/`、项目代码风格和现有命名约定
- 只改必要部分,不顺手重构无关内容
- 执行与改动相称的验证;如有相关测试且未被豁免,必须通过
- 遵循 `.gitattributes` 等换行与文件格式规则
### 决策与留痕
- 重要决策记录到 `memory-bank/decisions.md`
- 待确认事项在回复中显式列出
- `workflow-state``plan-status` 只能通过
`{{PLAYBOOK_SCRIPTS}}/main_loop.py` 维护
- `progress.md` 上半部分的人类摘要在阶段变化或执行结束后同步更新
- 同一错误重复两次以上时,立即更新
`AGENT_RULES.local.md``memory-bank/decisions.md`
- 发现项目特有规律时,沉淀到 `AGENT_RULES.local.md`
### Git 操作
- **不使用 --amend**:除非用户明确要求,总是创建新提交
- **不使用 --force**:特别是推送到 main/master如用户要求必须警告风险
- **不跳过 hooks**:不使用 `--no-verify`
- 不使用 `--amend`,除非用户明确要求
- 不使用 `--force`;如用户坚持,必须先说明风险
- 不使用 `--no-verify` 跳过 hooks
## 工具使用
### 工具使用
- **并行执行**:独立的工具调用尽可能并行执行
- **遵循 schema**:严格遵循工具参数定义
- **避免循环**:避免重复调用同一工具获取相同信息
- **优先专用工具**:文件操作用专用文件工具(非 cat/sed搜索用专用搜索工具非 grep/find
## Context 管理
以下情况应建议用户**开启新 Session**
- 当前方向明显跑偏,需要从头重新理解需求
- 讨论阶段产生了多个候选方案,进入执行阶段时应清空对话
- Session 过长导致注意力涣散,重复犯同类错误
> 新 Session 在干净 context 下工作效果更好;切换不是失败,是重置起点。
- 独立步骤尽可能并行执行
- 严格遵循工具参数定义与 schema
- 优先使用专用工具,不重复探测同一信息
- 文本搜索优先使用 `rg`
## 需要确认的场景
**常规模式**(可交互):
### 常规模式
- 需求不明确或存在多种可行方案
- 需要行为/兼容性取舍
- 风险或约束冲突
- **架构变更**:影响多个模块的修改
- **性能权衡**:需要在性能和可维护性之间选择
- **兼容性问题**:可能破坏现有用户代码
- 需求不明确,或存在多种可行方案
- 需要行为、兼容性或性能取舍
- 涉及架构变更、破坏性修改或约束冲突
- 风险较高,且继续执行可能放大返工成本
**无交互模式**(自动处理):
### 无交互模式
| 场景 | 处理方式 |
| -------------------------- | ---------------------------------- |
| 安全红线 | 立即停止,不继续后续 Plan |
| 架构变更/兼容性/破坏性修改 | 标记 blocked跳到下一个 Plan |
| 多种可行方案 | 选择最保守方案,记录选择理由到报告 |
| 歧义/风险/决策点 | 记录到报告,继续执行 |
- 安全红线:立即停止,不继续后续 Plan
- 架构变更、兼容性问题、破坏性修改:写回 `blocked`
- 多种可行方案:选择最保守方案,并在报告中说明理由
- 一般歧义、风险或决策点:记录到报告,继续执行安全部分
**可以不确认**(两种模式通用):
### 可以直接执行
- 明显的 bug 修复
- 符合现有模式的小改动
- 测试用例补充
- 测试用例补充或局部验证补齐
## Session 收尾
- 汇总已完成、阻塞、跳过的 Plan 及原因
- 标出需要其他环境处理的事项:`env:<环境>:<Task列表>`
- 必要时将重要结论写入 `memory-bank/decisions.md`
- 出现以下情况时,建议开启新 Session
- 当前方向明显跑偏
- 讨论阶段产出多个候选方案,准备进入执行
- Session 过长,开始重复犯同类错误
## 验证清单
每个 Plan 完成后,必须验证:
每个 Plan 完成后,至少确认
- [ ] 代码修改符合 `.agents/` 下的规则(如有)
- [ ] 相关测试通过(如有测试且未被豁免)
- [ ] 换行符正确
- [ ] 无语法错误
- [ ] 相关验证已执行,且测试在未豁免时通过
- [ ] 换行符与文件格式正确
- [ ] 无语法错误或明显运行时错误
- [ ] 已通过 `main_loop.py finish` 写回 Plan 状态
---

View File

@ -1,4 +1,54 @@
# Plan 状态
# 当前进展
<!--
填写指南:
- 上半部分给人类和 AI 快速恢复上下文
- 中间的 workflow-state 块记录当前阶段、spec、plan 与执行约束
- 下半部分的 plan-status 块由 main_loop.py 维护,是唯一权威状态源
-->
## Current Focus
- {{CURRENT_FOCUS}}
## Recent Changes
- {{RECENT_CHANGE_1}}
## Next Steps
1. {{NEXT_STEP_1}}
## Open Risks
- {{RISK_1}}
## 状态块示例
以下示例仅用于说明结构,真实状态由 `main_loop.py` 维护:
```text
## Workflow State
<!-- workflow-state:start -->
phase: planning
spec: docs/superpowers/specs/2026-05-18-demo-design.md
plan: docs/superpowers/plans/2026-05-18-demo.md
executor: executing-plans
constraints: karpathy-guidelines,.agents,AGENT_RULES
<!-- workflow-state:end -->
## Plan Status
<!-- plan-status:start -->
- [ ] `2026-05-18-demo.md` pending
<!-- plan-status:end -->
```
## Workflow State
<!-- workflow-state:start -->
<!-- workflow-state:end -->
## Plan Status
<!-- plan-status:start -->
<!-- plan-status:end -->

View File

@ -1,61 +1,49 @@
# 工作模式参考
# 工作流入口
<!--
本文件定义三种工作模式,供 AI 根据任务类型选择
核心规则(安全红线、验证清单等)见 AGENT_RULES.md。
本文件不重复定义核心规则;它只负责把任务路由到合适的工作流入口
安全红线、验证要求、主循环规则见 AGENT_RULES.md。
-->
## 模式 1: 探索模式Explore
## 路由原则
**目的**:理解代码库、分析问题、收集信息
- 需求不明确:先看 `docs/prompts/coding/clarify.md`
- 需要设计或拆解方案:走
`using-superpowers``$brainstorming``$writing-plans`
- `brainstorming` 结束后:立即
`playbook.py -record-spec <path> -progress memory-bank/progress.md`
- `writing-plans` 结束后:立即
`playbook.py -record-plan <path> -progress memory-bank/progress.md`
- 需要执行已有 Plan`main_loop.py claim`,再走
`$executing-plans`
- 如为代码类执行:在 `$executing-plans` 前强制叠加
`karpathy-guidelines`,并同时遵循 `.agents/``AGENT_RULES.md`
- 需要确认改动是否站得住:看 `docs/prompts/coding/verify-change.md`
- 一轮工作收尾:看 `docs/prompts/coding/close-task.md`
- 需要更新上下文:看 `docs/prompts/coding/update-memory.md`
- 需要评审 MR/PR`docs/prompts/coding/code-review.md`
**行为**
## 最小工作流
- 使用搜索工具探索代码
- 输出分析报告和发现
- 不修改任何代码
```text
需求不清 -> clarify
需求明确 -> using-superpowers / brainstorming / writing-plans
brainstorming 完成 -> record planning/spec
writing-plans 完成 -> record plan/executor/constraints
进入执行 -> claim -> executing-plans
代码执行 -> + karpathy-guidelines + .agents + AGENT_RULES
执行结束 -> finish -> update-memory
准备交付 -> verify-change
本轮结束 -> close-task
上下文变化 -> update-memory
```
**适用场景**
## 说明
- 理解某个模块的实现
- 分析 bug 的根本原因
- 评估功能实现的可行性
---
## 模式 2: 开发模式Develop
**目的**:实现功能、修复 bug、重构代码
**行为**
- 先读取相关文件,理解现有逻辑
- 进行精确修改
- 修改后运行测试验证
**适用场景**
- 实现新功能
- 修复已知 bug
- 优化性能
---
## 模式 3: 调试模式Debug
**目的**:诊断问题、对比差异、验证行为
**行为**
- 收集相关日志和输出
- 分析差异原因
- 修复后重新验证
**适用场景**
- 测试失败
- 输出不符合预期
- 性能问题诊断
- `prompts/` 是入口,不是规则权威
- 稳定约束写入 `memory-bank/``AGENT_RULES.local.md`
- 执行留痕以 `memory-bank/progress.md`
`workflow-state``plan-status` 为准
---

View File

@ -55,6 +55,65 @@ class PlaybookCliTests(unittest.TestCase):
self.assertEqual(result.returncode, 0)
self.assertIn("Usage:", result.stdout + result.stderr)
def test_record_spec_updates_progress_workflow_state(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text("# 当前进展\n", encoding="utf-8")
result = run_cli(
"-record-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
"-progress",
str(progress),
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
def test_record_plan_updates_progress_workflow_state(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Workflow State",
"",
"<!-- workflow-state:start -->",
"phase: planning",
"spec: docs/superpowers/specs/2026-05-18-demo-design.md",
"<!-- workflow-state:end -->",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"-record-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-progress",
str(progress),
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
def test_missing_config_is_error(self):
result = run_cli()
self.assertNotEqual(result.returncode, 0)

View File

@ -79,7 +79,7 @@ echo ""
echo "🔍 验证 memory-bank 模板"
MEMORY_BANK_DIR="$TEMPLATES_DIR/memory-bank"
for name in project-brief tech-stack architecture progress decisions; do
for name in project-brief tech-context system-patterns active-context progress decisions; do
validate_file_exists "$MEMORY_BANK_DIR/$name.template.md" "memory-bank/$name.template.md"
done
@ -90,9 +90,10 @@ PROMPTS_DIR="$TEMPLATES_DIR/prompts"
validate_file_exists "$PROMPTS_DIR/README.md" "prompts/README.md"
validate_file_exists "$PROMPTS_DIR/system/agent-behavior.template.md" "prompts/system/agent-behavior.template.md"
validate_file_exists "$PROMPTS_DIR/coding/clarify.template.md" "prompts/coding/clarify.template.md"
validate_file_exists "$PROMPTS_DIR/coding/review.template.md" "prompts/coding/review.template.md"
validate_file_exists "$PROMPTS_DIR/coding/verify-change.template.md" "prompts/coding/verify-change.template.md"
validate_file_exists "$PROMPTS_DIR/coding/close-task.template.md" "prompts/coding/close-task.template.md"
validate_file_exists "$PROMPTS_DIR/coding/update-memory.template.md" "prompts/coding/update-memory.template.md"
validate_file_exists "$PROMPTS_DIR/coding/code-review.template.md" "prompts/coding/code-review.template.md"
validate_file_exists "$PROMPTS_DIR/meta/prompt-generator.template.md" "prompts/meta/prompt-generator.template.md"
echo ""

View File

@ -1,13 +1,22 @@
import importlib.util
import os
import platform
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "main_loop.py"
_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", SCRIPT)
assert _SPEC and _SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(MAIN_LOOP)
def run_cli(*args, cwd=None):
return subprocess.run(
@ -29,7 +38,7 @@ class MainLoopCliTests(unittest.TestCase):
def test_claim_seeds_progress_and_marks_first_plan_in_progress(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir = root / "docs" / "superpowers" / "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")
@ -37,26 +46,78 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli(
"claim",
"-plans",
"docs/plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(result.stdout.strip(), "PLAN=docs/plans/2026-01-01-old.md")
self.assertEqual(
result.stdout.strip(),
"PLAN=docs/superpowers/plans/2026-01-01-old.md",
)
progress = root / "memory-bank" / "progress.md"
text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- workflow-state:start -->", text)
self.assertIn("<!-- workflow-state:end -->", text)
self.assertIn("phase: executing", text)
self.assertIn("plan: docs/superpowers/plans/2026-01-01-old.md", text)
self.assertIn("<!-- plan-status:start -->", text)
self.assertIn("<!-- plan-status:end -->", text)
self.assertIn("`2026-01-01-old.md` in-progress", text)
self.assertIn("`2026-01-02-new.md` pending", text)
def test_claim_preserves_human_progress_sections_when_plan_block_missing(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-demo.md").write_text("demo", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Current Focus",
"",
"- keep-this-focus",
"",
"## Recent Changes",
"",
"- keep-this-change",
"",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"claim",
"-plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("- keep-this-focus", text)
self.assertIn("- keep-this-change", text)
self.assertIn("<!-- workflow-state:start -->", text)
self.assertIn("<!-- plan-status:start -->", text)
self.assertIn("`2026-01-01-demo.md` in-progress", text)
def test_claim_returns_existing_in_progress_before_pending(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir = root / "docs" / "superpowers" / "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")
@ -81,19 +142,65 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli(
"claim",
"-plans",
"docs/plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(result.stdout.strip(), "PLAN=docs/plans/2026-01-01-a.md")
self.assertEqual(
result.stdout.strip(),
"PLAN=docs/superpowers/plans/2026-01-01-a.md",
)
def test_claim_skips_stale_progress_entries_for_deleted_plans(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-02-live.md").write_text("live", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- workflow-state:start -->",
"phase: planning",
"<!-- workflow-state:end -->",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-01-01-deleted.md` pending",
"- [ ] `2026-01-02-live.md` pending",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"claim",
"-plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(
result.stdout.strip(),
"PLAN=docs/superpowers/plans/2026-01-02-live.md",
)
def test_claim_resumes_env_blocked_plan_and_preserves_note(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8")
@ -118,7 +225,7 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli(
"claim",
"-plans",
"docs/plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
@ -129,7 +236,7 @@ class MainLoopCliTests(unittest.TestCase):
result.stdout.strip(),
"\n".join(
[
"PLAN=docs/plans/2026-01-05-env.md",
"PLAN=docs/superpowers/plans/2026-01-05-env.md",
f"NOTE={note}",
]
),
@ -160,7 +267,7 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli(
"finish",
"-plan",
"docs/plans/2026-01-03-demo.md",
"docs/superpowers/plans/2026-01-03-demo.md",
"-status",
"done",
"-progress",
@ -171,7 +278,306 @@ class MainLoopCliTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("- [x] `2026-01-03-demo.md` done", text)
self.assertEqual(text.count("2026-01-03-demo.md"), 1)
self.assertEqual(
text.count("- [x] `2026-01-03-demo.md` done"),
1,
)
def test_finish_updates_workflow_phase_and_preserves_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Workflow State",
"",
"<!-- workflow-state:start -->",
"phase: executing",
"spec: docs/superpowers/specs/2026-05-18-demo-design.md",
"plan: docs/superpowers/plans/2026-05-18-demo.md",
"executor: executing-plans",
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
"<!-- workflow-state:end -->",
"",
"## Plan Status",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-05-18-demo.md` in-progress",
"<!-- plan-status:end -->",
"",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: done", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
def test_record_updates_workflow_state_block(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Plan Status",
"",
"<!-- plan-status:start -->",
"<!-- plan-status:end -->",
"",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-executor",
"executing-plans",
"-constraints",
"karpathy-guidelines,.agents,AGENT_RULES",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- workflow-state:start -->", text)
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
def test_record_claim_finish_workflow_chain(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-05-18-demo.md").write_text("demo", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
result = run_cli(
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
result = run_cli(
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-executor",
"executing-plans",
"-constraints",
"karpathy-guidelines,.agents,AGENT_RULES",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
result = run_cli(
"claim",
"-plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: done", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
self.assertIn("- [x] `2026-05-18-demo.md` done", text)
def test_concurrent_record_preserves_spec_and_plan_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
original_load = MAIN_LOOP.load_progress_lines
first_load = {"seen": False}
gate = threading.Lock()
def delayed_load(progress_path):
lines = original_load(progress_path)
with gate:
if not first_load["seen"]:
first_load["seen"] = True
threading.Event().wait(0.2)
return lines
MAIN_LOOP.load_progress_lines = delayed_load
try:
threads = [
threading.Thread(
target=MAIN_LOOP.record_workflow_state,
args=(
progress,
"planning",
"docs/superpowers/specs/2026-05-18-demo-design.md",
None,
None,
None,
),
),
threading.Thread(
target=MAIN_LOOP.record_workflow_state,
args=(
progress,
"planning",
None,
"docs/superpowers/plans/2026-05-18-demo.md",
"executing-plans",
"karpathy-guidelines,.agents,AGENT_RULES",
),
),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
finally:
MAIN_LOOP.load_progress_lines = original_load
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
def test_cross_process_record_lock_preserves_spec_and_plan_metadata(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
slow_env = dict(os.environ)
slow_env["PLAYBOOK_MAIN_LOOP_HOLD_LOCK_MS"] = "300"
proc = subprocess.Popen(
[
sys.executable,
str(SCRIPT),
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-spec",
"docs/superpowers/specs/2026-05-18-demo-design.md",
],
cwd=root,
env=slow_env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
time.sleep(0.05)
result = run_cli(
"record",
"-progress",
"memory-bank/progress.md",
"-phase",
"planning",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-executor",
"executing-plans",
"-constraints",
"karpathy-guidelines,.agents,AGENT_RULES",
cwd=root,
)
stdout, stderr = proc.communicate(timeout=5)
self.assertEqual(proc.returncode, 0, msg=stderr or stdout)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text)
self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text)
self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES",
text,
)
if __name__ == "__main__":

View File

@ -42,12 +42,42 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
)
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
tech_stack_template = (
ROOT / "templates" / "memory-bank" / "tech-stack.template.md"
tech_context_template = (
ROOT / "templates" / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_stack_template)
self.assertNotIn("**主要语言**", tech_stack_template)
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_context_template)
update_memory_template = (
ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md"
).read_text(encoding="utf-8")
self.assertIn("workflow-state", update_memory_template)
self.assertIn("plan-status", update_memory_template)
close_task_template = (
ROOT / "templates" / "prompts" / "coding" / "close-task.template.md"
).read_text(encoding="utf-8")
self.assertIn("main_loop.py finish", close_task_template)
self.assertIn("workflow-state.phase", close_task_template)
verify_change_template = (
ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md"
).read_text(encoding="utf-8")
self.assertIn("workflow-state.phase", verify_change_template)
self.assertIn("plan-status", verify_change_template)
prompts_readme = (
ROOT / "templates" / "prompts" / "README.md"
).read_text(encoding="utf-8")
self.assertIn("playbook.py -record-spec", prompts_readme)
self.assertIn("playbook.py -record-plan", prompts_readme)
agent_behavior_template = (
ROOT / "templates" / "prompts" / "system" / "agent-behavior.template.md"
).read_text(encoding="utf-8")
self.assertIn("playbook.py -record-spec", agent_behavior_template)
self.assertIn("playbook.py -record-plan", agent_behavior_template)
def test_sync_templates_replaces_playbook_scripts_without_main_language_support(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@ -74,18 +104,28 @@ langs = [\"cpp\", \"tsl\"]
self.assertIn(".agents/cpp/index.md", text)
self.assertNotIn("{{MAIN_LANGUAGE}}", text)
tech_stack = Path(tmp_dir) / "memory-bank" / "tech-stack.md"
tech_stack_text = tech_stack.read_text(encoding="utf-8")
self.assertNotIn("{{LANGUAGE_1}}", tech_stack_text)
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_text)
self.assertNotIn("**主要语言**", tech_stack_text)
tech_context = Path(tmp_dir) / "memory-bank" / "tech-context.md"
tech_context_text = tech_context.read_text(encoding="utf-8")
self.assertNotIn("{{LANGUAGE_1}}", tech_context_text)
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_text)
self.assertNotIn("**主要语言**", tech_context_text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8")
self.assertIn("docs/standards/playbook/scripts/main_loop.py claim", rules_text)
self.assertIn(
"docs/standards/playbook/scripts/main_loop.py claim",
rules_text,
)
self.assertIn("docs/superpowers/plans", rules_text)
self.assertNotIn("plan_progress.py", rules_text)
self.assertIn("不得直接使用 `$executing-plans`", rules_text)
self.assertIn("不得直接使用 `$subagent-driven-development`", rules_text)
self.assertIn("记录 `phase=planning` 与 `spec=<path>`", rules_text)
self.assertIn(
"记录 `plan=<path>`、`executor=executing-plans`、",
rules_text,
)
self.assertIn("未领取 Plan 前,不得直接进入 `$executing-plans`", rules_text)
self.assertIn("默认执行使用 `$executing-plans`", rules_text)
self.assertIn("不是默认执行器", rules_text)
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
self.assertFalse(rules_text.endswith("\n\n"))
@ -134,6 +174,52 @@ langs = ["typescript"]
self.assertIn("`docs/standards/playbook/docs/typescript/", text)
self.assertNotIn("`docs/typescript/", text)
def test_sync_memory_bank_includes_active_context_and_human_readable_progress(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_rules]
[sync_memory_bank]
project_name = "MyProject"
"""
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)
active_context = Path(tmp_dir) / "memory-bank" / "active-context.md"
self.assertTrue(active_context.is_file())
progress = Path(tmp_dir) / "memory-bank" / "progress.md"
progress_text = progress.read_text(encoding="utf-8")
self.assertIn("## Current Focus", progress_text)
self.assertIn("## 状态块示例", progress_text)
self.assertIn("phase: planning", progress_text)
self.assertIn("executor: executing-plans", progress_text)
self.assertIn("<!-- workflow-state:start -->", progress_text)
self.assertIn("<!-- workflow-state:end -->", progress_text)
self.assertIn("## Plan Status", progress_text)
self.assertIn("<!-- plan-status:start -->", progress_text)
self.assertIn("<!-- plan-status:end -->", progress_text)
system_patterns = Path(tmp_dir) / "memory-bank" / "system-patterns.md"
system_patterns_text = system_patterns.read_text(encoding="utf-8")
self.assertIn("# 系统模式与约束", system_patterns_text)
self.assertIn("## 核心不变量", system_patterns_text)
agents_md = Path(tmp_dir) / "AGENTS.md"
agents_text = agents_md.read_text(encoding="utf-8")
self.assertIn("memory-bank/active-context.md", agents_text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8")
self.assertIn("memory-bank/active-context.md", rules_text)
if __name__ == "__main__":
unittest.main()