✨ feat(workflow): add superpowers planning and execution state tracking
This commit is contained in:
parent
c1702a667a
commit
234b335663
35
README.md
35
README.md
|
|
@ -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`;那只是你执行脚本的位置。
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 状态
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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` 为准
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue