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

View File

@ -1,12 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from contextlib import contextmanager
import os
import platform import platform
import re import re
import sys import sys
import threading
import time
from pathlib import Path from pathlib import Path
from typing import Optional 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_START = "<!-- plan-status:start -->"
PLAN_STATUS_END = "<!-- plan-status:end -->" 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_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$")
PLAN_LINE_RE = re.compile( PLAN_LINE_RE = re.compile(
r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` " r"^- \[(?P<check>[ xX])\] `(?P<plan>[^`]+)` "
@ -15,6 +31,9 @@ PLAN_LINE_RE = re.compile(
) )
FINISH_STATUSES = {"done", "blocked", "skipped"} FINISH_STATUSES = {"done", "blocked", "skipped"}
ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$") 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: 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 claim -plans <dir> -progress <file>\n"
" python scripts/main_loop.py finish -plan <path> -status <status> " " python scripts/main_loop.py finish -plan <path> -status <status> "
"-progress <file> [-note <text>]\n" "-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" " python scripts/main_loop.py -h\n"
"Options:\n" "Options:\n"
" -plans DIR\n" " -plans DIR\n"
" -plan PATH\n" " -plan PATH\n"
" -status done|blocked|skipped\n" " -status done|blocked|skipped\n"
" -progress FILE\n" " -progress FILE\n"
" -phase brainstorming|planning|executing|done|blocked\n"
" -spec PATH\n"
" -executor NAME\n"
" -constraints CSV\n"
" -note TEXT\n" " -note TEXT\n"
" -h, -help Show this help.\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: def normalize_plan_key(plan_value: str) -> str:
raw = plan_value.strip().replace("\\", "/") raw = plan_value.strip().replace("\\", "/")
raw = raw.lstrip("./") raw = raw.lstrip("./")
if raw.startswith("docs/plans/"): if raw.startswith("docs/superpowers/plans/"):
return raw[len("docs/plans/") :] return raw[len("docs/superpowers/plans/") :]
marker = "/docs/plans/" marker = "/docs/superpowers/plans/"
if marker in raw: if marker in raw:
return raw.split(marker, 1)[1] return raw.split(marker, 1)[1]
return raw return raw
@ -98,6 +124,22 @@ def find_block(lines: list[str]) -> Optional[tuple[int, int]]:
return None 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( def parse_entries(
lines: list[str], start_idx: int, end_idx: int lines: list[str], start_idx: int, end_idx: int
) -> list[tuple[str, str, Optional[str], int]]: ) -> list[tuple[str, str, Optional[str], int]]:
@ -115,13 +157,60 @@ def parse_entries(
def render_progress_lines(plans: list[str]) -> list[str]: 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: for plan_key in plans:
lines.append(render_plan_line(plan_key, "pending", None)) lines.append(render_plan_line(plan_key, "pending", None))
lines.append(PLAN_STATUS_END) lines.append(PLAN_STATUS_END)
return lines 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]]: def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]:
if not note: if not note:
return None 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") 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( def ensure_plan_block(
lines: list[str], progress_path: Path, plan_keys: list[str] lines: list[str], progress_path: Path, plan_keys: list[str]
) -> tuple[list[str], int, int]: ) -> tuple[list[str], int, int]:
block = find_block(lines) lines, _, _ = ensure_workflow_state_block(lines)
if not block: lines, start_idx, end_idx = ensure_block_with_lines(
lines = render_progress_lines(plan_keys) lines,
write_progress_lines(progress_path, lines) PLAN_STATUS_START,
block = find_block(lines) PLAN_STATUS_END,
if not block: [PLAN_STATUS_START, PLAN_STATUS_END],
raise ValueError("failed to create plan status block") "## Plan Status",
return lines, block[0], block[1] )
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( def ensure_all_plans_present(
@ -175,6 +379,13 @@ def ensure_all_plans_present(
return entries 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( def choose_claim_entry(
entries: list[tuple[str, str, Optional[str], int]], current_env: Optional[str] entries: list[tuple[str, str, Optional[str], int]], current_env: Optional[str]
) -> Optional[tuple[str, Optional[str], int]]: ) -> 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: if not plan_keys:
return 2, "ERROR: no plan files found" return 2, "ERROR: no plan files found"
lines = load_progress_lines(progress_path) with locked_progress(progress_path):
try: lines = load_progress_lines(progress_path)
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, plan_keys) try:
except ValueError as exc: lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, plan_keys)
return 2, f"ERROR: {exc}" except ValueError as exc:
return 2, f"ERROR: {exc}"
entries = ensure_all_plans_present(lines, start_idx, end_idx, progress_path, plan_keys) entries = ensure_all_plans_present(lines, start_idx, end_idx, progress_path, plan_keys)
chosen = choose_claim_entry(entries, detect_env()) entries = filter_existing_entries(entries, plan_keys)
if not chosen: chosen = choose_claim_entry(entries, detect_env())
return 2, "ERROR: no claimable plans" if not chosen:
return 0, "NOOP: no claimable plans"
plan_key, note, idx = chosen plan_key, note, idx = chosen
lines[idx] = render_plan_line(plan_key, "in-progress", note) lines[idx] = render_plan_line(plan_key, "in-progress", note)
write_progress_lines(progress_path, lines) 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()}"] output = [f"PLAN={(plans_dir / plan_key).as_posix()}"]
if note: if note:
@ -234,27 +452,58 @@ def finish_plan(
if not plan: if not plan:
return 2, "ERROR: plan is required" return 2, "ERROR: plan is required"
lines = load_progress_lines(progress_path)
plan_key = normalize_plan_key(plan) plan_key = normalize_plan_key(plan)
with locked_progress(progress_path):
lines = load_progress_lines(progress_path)
try: try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, [plan_key]) lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, [plan_key])
except ValueError as exc: except ValueError as exc:
return 2, f"ERROR: {exc}" return 2, f"ERROR: {exc}"
entries = parse_entries(lines, start_idx, end_idx) entries = parse_entries(lines, start_idx, end_idx)
rendered_note = normalize_note(note) if note else None rendered_note = normalize_note(note) if note else None
updated_line = render_plan_line(plan_key, status, rendered_note) updated_line = render_plan_line(plan_key, status, rendered_note)
for entry_plan, _, _, idx in entries: for entry_plan, _, _, idx in entries:
if entry_plan == plan_key: if entry_plan == plan_key:
lines[idx] = updated_line lines[idx] = updated_line
write_progress_lines(progress_path, lines) workflow_phase = "done" if status == "done" else "blocked"
return 0, updated_line 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] lines[end_idx:end_idx] = [updated_line]
write_progress_lines(progress_path, lines) workflow_phase = "done" if status == "done" else "blocked"
return 0, updated_line 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: def main(argv: list[str]) -> int:
@ -266,7 +515,7 @@ def main(argv: list[str]) -> int:
return 0 return 0
mode = argv[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(f"ERROR: unknown mode: {mode}", file=sys.stderr)
print(usage(), file=sys.stderr) print(usage(), file=sys.stderr)
return 2 return 2
@ -295,6 +544,26 @@ def main(argv: list[str]) -> int:
print(message) print(message)
return 0 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") plan = flags.get("-plan")
status = flags.get("-status") status = flags.get("-status")
progress = flags.get("-progress") progress = flags.get("-progress")

View File

@ -5,6 +5,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from shutil import copy2, copytree, rmtree, which from shutil import copy2, copytree, rmtree, which
import subprocess import subprocess
import importlib.util
try: try:
import tomllib import tomllib
@ -22,6 +23,11 @@ ORDER = [
] ]
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
PLAYBOOK_ROOT = SCRIPT_DIR.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"} PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home", "skill_link"}
DOCS_INDEX_SECTION_HEADINGS = { DOCS_INDEX_SECTION_HEADINGS = {
"common": "## 跨语言common", "common": "## 跨语言common",
@ -34,7 +40,23 @@ DOCS_INDEX_SECTION_HEADINGS = {
def usage() -> str: 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: 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: if "-h" in argv or "-help" in argv:
print(usage()) print(usage())
return 0 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: if "-config" not in argv:
print("ERROR: -config is required.\n" + usage(), file=sys.stderr) print("ERROR: -config is required.\n" + usage(), file=sys.stderr)
return 2 return 2

View File

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

View File

@ -9,235 +9,278 @@
3. 仓库规则:`.agents/` 与 `AGENTS.md` 3. 仓库规则:`.agents/` 与 `AGENTS.md`
4. 本文件 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 要求
| 阶段 | 工具 | 产出 | 留痕 | - `Plan Meta` 必填,位于 Plan 头部 `---` 之后、Task 1 之前
| ------------ | ---------------------- | ----------------- | -------------------------- | - `Plan Meta` 至少包含:
| 头脑风暴 | `$brainstorming` skill | 设计思路 | 无 | - `Plan Group`
| 生成计划 | `$writing-plans` skill | `docs/plans/*.md` | 无 | - `Parent Plan`
| **执行计划** | **`main_loop.py` 主循环** | 代码/配置变更 | **`memory-bank/progress.md`** | - `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`:永久跳过,不再执行
| 状态 | 含义 | `skipped` 如需恢复,必须手动改回 `pending`
| ----------- | ------------------------- |
| pending | 待执行 |
| in-progress | 执行中(崩溃恢复用) |
| done | 已完成 |
| blocked | 阻塞(需人工介入) |
| skipped | 跳过Plan 不再需要执行) |
> 说明:`skipped` 仅用于永久不再执行;如需恢复执行,需手动改回 `pending` ### 环境阻塞格式
**环境阻塞格式**`blocked: env:<环境>:<Task列表>` - 格式:`env:<环境>:<Task列表>`
- 示例:`env:windows:Task2,Task4`
- `Task` 列表必须使用英文逗号分隔,且不要包含空格
- 示例:`blocked: env:windows:Task2,Task4` ### 领取与写回
- 含义:需要在指定环境执行列出的 Task
- 约束:`Task` 列表使用英文逗号分隔,不要包含空格,便于解析
**流程** 领取命令:
1. 领取 Plan ```bash
- 运行 `python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim -plans docs/plans -progress memory-bank/progress.md` python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim \
- 该命令会**原子化**完成三件事: -plans docs/superpowers/plans \
- 自动识别当前环境(`windows` / `linux` / `darwin` -progress memory-bank/progress.md
- 选择第一个可执行的 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. **结束**:主循环终止
## Plan 规则 该命令会在锁保护下串行完成三件事:
- **Plan Meta 必填**Plan 头部 `---` 之后、Task 1 之前插入 `## Plan Meta`,包含: - 自动识别当前环境:`windows`、`linux`、`darwin`
- `Plan Group`(归类任务) - 按顺序选择可执行 Plan
- `Parent Plan`(上层/集成计划链接) `in-progress` > `pending` > `blocked: env:<当前环境>:...`
- `Verification Scope`local 或 integration - 将选中的 Plan 写成 `in-progress`
- `Verification Gate`must-pass
- **不允许中断任务**Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 `$brainstorming` 阶段解决后再产出 Plan
- **验证必须可通过**Plan 内验证应为当前阶段可通过的局部验证;需要集成验证的内容放入上层/集成 Plan
- **执行入口唯一**Plan 生成完成后,后续执行只能由主循环驱动;不得按 Plan 头部说明直接切换到 `$executing-plans``$subagent-driven-development`
- 不因等待确认而中断可执行步骤;待确认事项在回复中列出
- 每轮只处理一个 Plan
- **小步快跑**:每个 Plan 应该可快速完成
- **可验证**:每个 Plan 必须包含验证步骤
## 复利工程 这里的锁保护的是 `progress.md` 状态块更新,避免多个 session
同时读写时发生覆盖。
每次 Session 结束时 stdout 必须包含:
- **同一错误发生 2 次以上** → 立即更新 `AGENT_RULES.local.md``memory-bank/decisions.md`,避免下次重蹈 - `PLAN=<path>`
- **发现项目特有规律**(如特定模块的注意事项、常见陷阱)→ 沉淀到 `AGENT_RULES.local.md` - 如为环境恢复,还会附带 `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 格式) ```bash
- **待确认事项**:在回复中列出并等待确认 python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
- **进度留痕**:通过 `{{PLAYBOOK_SCRIPTS}}/main_loop.py` 维护 `memory-bank/progress.md` 的 Plan 状态块(唯一权威) -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 操作 ### Git 操作
- **不使用 --amend**:除非用户明确要求,总是创建新提交 - 不使用 `--amend`,除非用户明确要求
- **不使用 --force**:特别是推送到 main/master如用户要求必须警告风险 - 不使用 `--force`;如用户坚持,必须先说明风险
- **不跳过 hooks**:不使用 `--no-verify` - 不使用 `--no-verify` 跳过 hooks
## 工具使用 ### 工具使用
- **并行执行**:独立的工具调用尽可能并行执行 - 独立步骤尽可能并行执行
- **遵循 schema**:严格遵循工具参数定义 - 严格遵循工具参数定义与 schema
- **避免循环**:避免重复调用同一工具获取相同信息 - 优先使用专用工具,不重复探测同一信息
- **优先专用工具**:文件操作用专用文件工具(非 cat/sed搜索用专用搜索工具非 grep/find - 文本搜索优先使用 `rg`
## Context 管理
以下情况应建议用户**开启新 Session**
- 当前方向明显跑偏,需要从头重新理解需求
- 讨论阶段产生了多个候选方案,进入执行阶段时应清空对话
- Session 过长导致注意力涣散,重复犯同类错误
> 新 Session 在干净 context 下工作效果更好;切换不是失败,是重置起点。
## 需要确认的场景 ## 需要确认的场景
**常规模式**(可交互): ### 常规模式
- 需求不明确或存在多种可行方案 - 需求不明确,或存在多种可行方案
- 需要行为/兼容性取舍 - 需要行为、兼容性或性能取舍
- 风险或约束冲突 - 涉及架构变更、破坏性修改或约束冲突
- **架构变更**:影响多个模块的修改 - 风险较高,且继续执行可能放大返工成本
- **性能权衡**:需要在性能和可维护性之间选择
- **兼容性问题**:可能破坏现有用户代码
**无交互模式**(自动处理): ### 无交互模式
| 场景 | 处理方式 | - 安全红线:立即停止,不继续后续 Plan
| -------------------------- | ---------------------------------- | - 架构变更、兼容性问题、破坏性修改:写回 `blocked`
| 安全红线 | 立即停止,不继续后续 Plan | - 多种可行方案:选择最保守方案,并在报告中说明理由
| 架构变更/兼容性/破坏性修改 | 标记 blocked跳到下一个 Plan | - 一般歧义、风险或决策点:记录到报告,继续执行安全部分
| 多种可行方案 | 选择最保守方案,记录选择理由到报告 |
| 歧义/风险/决策点 | 记录到报告,继续执行 |
**可以不确认**(两种模式通用): ### 可以直接执行
- 明显的 bug 修复 - 明显的 bug 修复
- 符合现有模式的小改动 - 符合现有模式的小改动
- 测试用例补充 - 测试用例补充或局部验证补齐
## Session 收尾
- 汇总已完成、阻塞、跳过的 Plan 及原因
- 标出需要其他环境处理的事项:`env:<环境>:<Task列表>`
- 必要时将重要结论写入 `memory-bank/decisions.md`
- 出现以下情况时,建议开启新 Session
- 当前方向明显跑偏
- 讨论阶段产出多个候选方案,准备进入执行
- Session 过长,开始重复犯同类错误
## 验证清单 ## 验证清单
每个 Plan 完成后,必须验证: 每个 Plan 完成后,至少确认
- [ ] 代码修改符合 `.agents/` 下的规则(如有) - [ ] 代码修改符合 `.agents/` 下的规则(如有)
- [ ] 相关测试通过(如有测试且未被豁免) - [ ] 相关验证已执行,且测试在未豁免时通过
- [ ] 换行符正确 - [ ] 换行符与文件格式正确
- [ ] 无语法错误 - [ ] 无语法错误或明显运行时错误
- [ ] 已通过 `main_loop.py finish` 写回 Plan 状态 - [ ] 已通过 `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:start -->
<!-- plan-status:end --> <!-- 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
```
**适用场景** ## 说明
- 理解某个模块的实现 - `prompts/` 是入口,不是规则权威
- 分析 bug 的根本原因 - 稳定约束写入 `memory-bank/``AGENT_RULES.local.md`
- 评估功能实现的可行性 - 执行留痕以 `memory-bank/progress.md`
`workflow-state``plan-status` 为准
---
## 模式 2: 开发模式Develop
**目的**:实现功能、修复 bug、重构代码
**行为**
- 先读取相关文件,理解现有逻辑
- 进行精确修改
- 修改后运行测试验证
**适用场景**
- 实现新功能
- 修复已知 bug
- 优化性能
---
## 模式 3: 调试模式Debug
**目的**:诊断问题、对比差异、验证行为
**行为**
- 收集相关日志和输出
- 分析差异原因
- 修复后重新验证
**适用场景**
- 测试失败
- 输出不符合预期
- 性能问题诊断
--- ---

View File

@ -55,6 +55,65 @@ class PlaybookCliTests(unittest.TestCase):
self.assertEqual(result.returncode, 0) self.assertEqual(result.returncode, 0)
self.assertIn("Usage:", result.stdout + result.stderr) 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): def test_missing_config_is_error(self):
result = run_cli() result = run_cli()
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)

View File

@ -79,7 +79,7 @@ echo ""
echo "🔍 验证 memory-bank 模板" echo "🔍 验证 memory-bank 模板"
MEMORY_BANK_DIR="$TEMPLATES_DIR/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" validate_file_exists "$MEMORY_BANK_DIR/$name.template.md" "memory-bank/$name.template.md"
done 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/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/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/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/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 "" echo ""

View File

@ -1,13 +1,22 @@
import importlib.util
import os
import platform import platform
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
import time
import unittest import unittest
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "main_loop.py" 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): def run_cli(*args, cwd=None):
return subprocess.run( return subprocess.run(
@ -29,7 +38,7 @@ class MainLoopCliTests(unittest.TestCase):
def test_claim_seeds_progress_and_marks_first_plan_in_progress(self): def test_claim_seeds_progress_and_marks_first_plan_in_progress(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
plans_dir = root / "docs" / "plans" plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True) plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-old.md").write_text("old", encoding="utf-8") (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") (plans_dir / "2026-01-02-new.md").write_text("new", encoding="utf-8")
@ -37,26 +46,78 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli( result = run_cli(
"claim", "claim",
"-plans", "-plans",
"docs/plans", "docs/superpowers/plans",
"-progress", "-progress",
"memory-bank/progress.md", "memory-bank/progress.md",
cwd=root, cwd=root,
) )
self.assertEqual(result.returncode, 0, msg=result.stderr) 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" progress = root / "memory-bank" / "progress.md"
text = progress.read_text(encoding="utf-8") 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:start -->", text)
self.assertIn("<!-- plan-status:end -->", text) self.assertIn("<!-- plan-status:end -->", text)
self.assertIn("`2026-01-01-old.md` in-progress", text) self.assertIn("`2026-01-01-old.md` in-progress", text)
self.assertIn("`2026-01-02-new.md` pending", 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): def test_claim_returns_existing_in_progress_before_pending(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
plans_dir = root / "docs" / "plans" plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True) plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-a.md").write_text("a", encoding="utf-8") (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") (plans_dir / "2026-01-02-b.md").write_text("b", encoding="utf-8")
@ -81,19 +142,65 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli( result = run_cli(
"claim", "claim",
"-plans", "-plans",
"docs/plans", "docs/superpowers/plans",
"-progress", "-progress",
"memory-bank/progress.md", "memory-bank/progress.md",
cwd=root, cwd=root,
) )
self.assertEqual(result.returncode, 0, msg=result.stderr) 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): def test_claim_resumes_env_blocked_plan_and_preserves_note(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
plans_dir = root / "docs" / "plans" plans_dir = root / "docs" / "superpowers" / "plans"
plans_dir.mkdir(parents=True) plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8") (plans_dir / "2026-01-05-env.md").write_text("env", encoding="utf-8")
@ -118,7 +225,7 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli( result = run_cli(
"claim", "claim",
"-plans", "-plans",
"docs/plans", "docs/superpowers/plans",
"-progress", "-progress",
"memory-bank/progress.md", "memory-bank/progress.md",
cwd=root, cwd=root,
@ -129,7 +236,7 @@ class MainLoopCliTests(unittest.TestCase):
result.stdout.strip(), result.stdout.strip(),
"\n".join( "\n".join(
[ [
"PLAN=docs/plans/2026-01-05-env.md", "PLAN=docs/superpowers/plans/2026-01-05-env.md",
f"NOTE={note}", f"NOTE={note}",
] ]
), ),
@ -160,7 +267,7 @@ class MainLoopCliTests(unittest.TestCase):
result = run_cli( result = run_cli(
"finish", "finish",
"-plan", "-plan",
"docs/plans/2026-01-03-demo.md", "docs/superpowers/plans/2026-01-03-demo.md",
"-status", "-status",
"done", "done",
"-progress", "-progress",
@ -171,7 +278,306 @@ class MainLoopCliTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn("- [x] `2026-01-03-demo.md` done", text) 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__": if __name__ == "__main__":

View File

@ -42,12 +42,42 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
) )
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template) self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
tech_stack_template = ( tech_context_template = (
ROOT / "templates" / "memory-bank" / "tech-stack.template.md" ROOT / "templates" / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_template) self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_stack_template) self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_stack_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): def test_sync_templates_replaces_playbook_scripts_without_main_language_support(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
@ -74,18 +104,28 @@ langs = [\"cpp\", \"tsl\"]
self.assertIn(".agents/cpp/index.md", text) self.assertIn(".agents/cpp/index.md", text)
self.assertNotIn("{{MAIN_LANGUAGE}}", text) self.assertNotIn("{{MAIN_LANGUAGE}}", text)
tech_stack = Path(tmp_dir) / "memory-bank" / "tech-stack.md" tech_context = Path(tmp_dir) / "memory-bank" / "tech-context.md"
tech_stack_text = tech_stack.read_text(encoding="utf-8") tech_context_text = tech_context.read_text(encoding="utf-8")
self.assertNotIn("{{LANGUAGE_1}}", tech_stack_text) self.assertNotIn("{{LANGUAGE_1}}", tech_context_text)
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_text) self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_text)
self.assertNotIn("**主要语言**", tech_stack_text) self.assertNotIn("**主要语言**", tech_context_text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md" rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8") 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.assertNotIn("plan_progress.py", rules_text)
self.assertIn("不得直接使用 `$executing-plans`", rules_text) self.assertIn("记录 `phase=planning` 与 `spec=<path>`", rules_text)
self.assertIn("不得直接使用 `$subagent-driven-development`", 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.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
self.assertFalse(rules_text.endswith("\n\n")) self.assertFalse(rules_text.endswith("\n\n"))
@ -134,6 +174,52 @@ langs = ["typescript"]
self.assertIn("`docs/standards/playbook/docs/typescript/", text) self.assertIn("`docs/standards/playbook/docs/typescript/", text)
self.assertNotIn("`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__": if __name__ == "__main__":
unittest.main() unittest.main()