From 234b33566355f4988ff92ab066a78106fd0bb829 Mon Sep 17 00:00:00 2001 From: csh Date: Mon, 18 May 2026 16:28:06 +0800 Subject: [PATCH] :sparkles: feat(workflow): add superpowers planning and execution state tracking --- README.md | 35 +- scripts/main_loop.py | 351 ++++++++++++-- scripts/playbook.py | 65 ++- templates/AGENTS.template.md | 17 +- templates/AGENT_RULES.template.md | 385 +++++++++------- templates/memory-bank/progress.template.md | 52 ++- .../prompts/system/agent-behavior.template.md | 86 ++-- tests/cli/test_playbook_cli.py | 59 +++ tests/templates/validate_project_templates.sh | 7 +- tests/test_main_loop_cli.py | 428 +++++++++++++++++- tests/test_sync_templates_placeholders.py | 112 ++++- 11 files changed, 1287 insertions(+), 310 deletions(-) diff --git a/README.md b/README.md index e266a5d5..57a2f352 100644 --- a/README.md +++ b/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 /scripts/playbook.py \ + -record-spec docs/superpowers/specs/-design.md \ + -progress memory-bank/progress.md + +# plan 写完后 +python /scripts/playbook.py \ + -record-plan docs/superpowers/plans/.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`;那只是你执行脚本的位置。 diff --git a/scripts/main_loop.py b/scripts/main_loop.py index f7acbff2..a2fd8ad4 100644 --- a/scripts/main_loop.py +++ b/scripts/main_loop.py @@ -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_END = "" +WORKFLOW_STATE_START = "" +WORKFLOW_STATE_END = "" PLAN_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$") PLAN_LINE_RE = re.compile( r"^- \[(?P[ xX])\] `(?P[^`]+)` " @@ -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 -progress \n" " python scripts/main_loop.py finish -plan -status " "-progress [-note ]\n" + " python scripts/main_loop.py record -progress -phase " + "[-spec ] [-plan ] [-executor ] " + "[-constraints ]\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") diff --git a/scripts/playbook.py b/scripts/playbook.py index c6b406a0..cee47c80 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -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 \n python scripts/playbook.py -h" + return ( + "Usage:\n" + " python scripts/playbook.py -config \n" + " python scripts/playbook.py -record-spec -progress \n" + " python scripts/playbook.py -record-plan -progress \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 diff --git a/templates/AGENTS.template.md b/templates/AGENTS.template.md index ef99cacf..911ff687 100644 --- a/templates/AGENTS.template.md +++ b/templates/AGENTS.template.md @@ -6,10 +6,7 @@ -请以 `.agents/` 下的规则为准: - -- 入口:`.agents/index.md` -- 语言规则:见 `.agents/index.md` 与对应语言子目录 +- [.agents/index.md](.agents/index.md) - 语言规则与工具入口 @@ -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) - 提示词与流程入口 diff --git a/templates/AGENT_RULES.template.md b/templates/AGENT_RULES.template.md index 02f40208..9ad32e7d 100644 --- a/templates/AGENT_RULES.template.md +++ b/templates/AGENT_RULES.template.md @@ -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=` +- `writing-plans` 写出 plan 后,立即用 `playbook.py -record-plan` + 记录 `plan=`、`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:<环境>:` +- 格式:`env:<环境>:` +- 示例:`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=`;如果是从环境阻塞恢复,还会附带 `NOTE=env:<环境>:` - - 如无可执行 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 -status done -progress memory-bank/progress.md` - - 有 Task 因环境跳过: - - `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan -status blocked -progress memory-bank/progress.md -note "env:<所需环境>:"` - - 其他阻塞: - - `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -plan -status blocked -progress memory-bank/progress.md -note "<原因>"` - - 跳过整个 Plan: - - `python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish -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=` +- 如为环境恢复,还会附带 `NOTE=env:<环境>:` -> 目标:让每次 Session 的起点比上次更高。 +规划与执行留痕示例: -## 执行约束 +```bash +# brainstorming 完成后 +python {{PLAYBOOK_SCRIPTS}}/playbook.py \ + -record-spec docs/superpowers/specs/-design.md \ + -progress memory-bank/progress.md +``` -### 代码修改 +```bash +# writing-plans 完成后 +python {{PLAYBOOK_SCRIPTS}}/playbook.py \ + -record-plan docs/superpowers/plans/.md \ + -progress memory-bank/progress.md +``` -- **必须先读文件再修改**:不读文件就提议修改是禁止的 -- **必须运行测试验证**:相关测试必须通过 -- **遵循换行规则**:遵循 `.gitattributes` 规则 -- **命名一致性**:遵循项目现有的命名风格 -- **最小改动原则**:只修改必要的部分,不顺手重构 +写回命令示例: -### 决策记录 +```bash +python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \ + -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 \ + -status blocked \ + -progress memory-bank/progress.md \ + -note "env:<所需环境>:" +``` + +```bash +python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \ + -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:<环境>:` +- 必要时将重要结论写入 `memory-bank/decisions.md` +- 出现以下情况时,建议开启新 Session: + - 当前方向明显跑偏 + - 讨论阶段产出多个候选方案,准备进入执行 + - Session 过长,开始重复犯同类错误 ## 验证清单 -每个 Plan 完成后,必须验证: +每个 Plan 完成后,至少确认: - [ ] 代码修改符合 `.agents/` 下的规则(如有) -- [ ] 相关测试通过(如有测试且未被豁免) -- [ ] 换行符正确 -- [ ] 无语法错误 +- [ ] 相关验证已执行,且测试在未豁免时通过 +- [ ] 换行符与文件格式正确 +- [ ] 无语法错误或明显运行时错误 - [ ] 已通过 `main_loop.py finish` 写回 Plan 状态 --- diff --git a/templates/memory-bank/progress.template.md b/templates/memory-bank/progress.template.md index 559d39b4..cfcd500a 100644 --- a/templates/memory-bank/progress.template.md +++ b/templates/memory-bank/progress.template.md @@ -1,4 +1,54 @@ -# Plan 状态 +# 当前进展 + + + +## 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 + +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 + + +## Plan Status + +- [ ] `2026-05-18-demo.md` pending + +``` + +## Workflow State + + + + +## Plan Status diff --git a/templates/prompts/system/agent-behavior.template.md b/templates/prompts/system/agent-behavior.template.md index 5bad0089..64879b3b 100644 --- a/templates/prompts/system/agent-behavior.template.md +++ b/templates/prompts/system/agent-behavior.template.md @@ -1,61 +1,49 @@ -# 工作模式参考 +# 工作流入口 -## 模式 1: 探索模式(Explore) +## 路由原则 -**目的**:理解代码库、分析问题、收集信息 +- 需求不明确:先看 `docs/prompts/coding/clarify.md` +- 需要设计或拆解方案:走 + `using-superpowers` → `$brainstorming` → `$writing-plans` +- `brainstorming` 结束后:立即 + `playbook.py -record-spec -progress memory-bank/progress.md` +- `writing-plans` 结束后:立即 + `playbook.py -record-plan -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` 为准 --- diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index d8a7dc4e..db7c2f66 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -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", + "", + "", + "phase: planning", + "spec: docs/superpowers/specs/2026-05-18-demo-design.md", + "", + ] + ) + + "\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) diff --git a/tests/templates/validate_project_templates.sh b/tests/templates/validate_project_templates.sh index 341ddb80..044d9911 100644 --- a/tests/templates/validate_project_templates.sh +++ b/tests/templates/validate_project_templates.sh @@ -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 "" diff --git a/tests/test_main_loop_cli.py b/tests/test_main_loop_cli.py index 66008174..8c9e9192 100644 --- a/tests/test_main_loop_cli.py +++ b/tests/test_main_loop_cli.py @@ -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("", text) + self.assertIn("", text) + self.assertIn("phase: executing", text) + self.assertIn("plan: docs/superpowers/plans/2026-01-01-old.md", text) self.assertIn("", text) self.assertIn("", 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("", text) + self.assertIn("", 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 状态", + "", + "", + "phase: planning", + "", + "", + "", + "- [ ] `2026-01-01-deleted.md` pending", + "- [ ] `2026-01-02-live.md` pending", + "", + "", + ] + ), + 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", + "", + "", + "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", + "", + "", + "## Plan Status", + "", + "", + "- [ ] `2026-05-18-demo.md` in-progress", + "", + "", + ] + ) + + "\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", + "", + "", + "", + "", + ] + ) + + "\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("", 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__": diff --git a/tests/test_sync_templates_placeholders.py b/tests/test_sync_templates_placeholders.py index 1c9e33ac..e2372aca 100644 --- a/tests/test_sync_templates_placeholders.py +++ b/tests/test_sync_templates_placeholders.py @@ -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=`", rules_text) + self.assertIn( + "记录 `plan=`、`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("", progress_text) + self.assertIn("", progress_text) + self.assertIn("## Plan Status", progress_text) + self.assertIn("", progress_text) + self.assertIn("", 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()