From 1911d2dda3aebfec8342bac9a307e46cdf0c02c6 Mon Sep 17 00:00:00 2001 From: csh Date: Fri, 26 Jun 2026 17:10:09 +0800 Subject: [PATCH] :wastebasket: remove(progress): drop workflow state tracking Use plan-status as the only machine state source for plan queueing, claiming, and finishing. Route record-plan through queue insertion and update templates/prompts to match the single-state model. BREAKING CHANGE: playbook.py -record-spec and main_loop.py record are removed. --- scripts/main_loop.py | 253 +++-------------- scripts/playbook.py | 30 +- templates/AGENT_RULES.template.md | 26 +- templates/README.md | 2 +- templates/memory-bank/progress.template.md | 19 +- .../prompts/coding/close-task.template.md | 15 +- .../prompts/coding/update-memory.template.md | 9 +- .../prompts/coding/verify-change.template.md | 9 +- test/cli/test_playbook_cli.py | 64 +++-- test/test_main_loop_cli.py | 261 ++++++++---------- test/test_sync_templates_placeholders.py | 17 +- 11 files changed, 243 insertions(+), 462 deletions(-) diff --git a/scripts/main_loop.py b/scripts/main_loop.py index 01fd0c25..d1add1d8 100644 --- a/scripts/main_loop.py +++ b/scripts/main_loop.py @@ -38,24 +38,6 @@ PLAN_META_REQUIRED_FIELDS = ( "Verification Scope", "Verification Gate", ) -WORKFLOW_STATE_KEYS = { - "phase", - "spec", - "plan", - "executor", - "constraints", - "claimed_by", - "claimed_at", - "verification", -} -WORKFLOW_PHASES = { - "brainstorming", - "planning", - "executing", - "done", - "blocked", - "skipped", -} THREAD_LOCKS: dict[str, threading.Lock] = {} THREAD_LOCKS_GUARD = threading.Lock() @@ -67,19 +49,12 @@ def usage() -> str: " python scripts/main_loop.py finish -plan -status " "-progress [-note ] [-verified ]\n" " python scripts/main_loop.py status -plans -progress \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|skipped\n" - " -spec PATH\n" - " -executor NAME\n" - " -constraints CSV\n" " -note TEXT\n" " -owner NAME\n" " -verified TEXT\n" @@ -205,11 +180,6 @@ def render_progress_lines(plans: list[str]) -> list[str]: lines = [ "# 当前进展", "", - "## Workflow State", - "", - WORKFLOW_STATE_START, - WORKFLOW_STATE_END, - "", "## Plan Status", "", PLAN_STATUS_START, @@ -220,47 +190,6 @@ def render_progress_lines(plans: list[str]) -> list[str]: 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, - claimed_by: Optional[str] = None, - claimed_at: Optional[str] = None, - verification: Optional[str] = None, -) -> list[str]: - lines = [WORKFLOW_STATE_START] - for key, value in ( - ("phase", phase), - ("spec", spec), - ("plan", plan), - ("executor", executor), - ("constraints", constraints), - ("claimed_by", claimed_by), - ("claimed_at", claimed_at), - ("verification", verification), - ): - if value: - lines.append(f"{key}: {value}") - 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 WORKFLOW_STATE_KEYS: - state[key] = value - return state - - def parse_env_blocked_note(note: Optional[str]) -> Optional[tuple[str, str]]: if not note: return None @@ -360,7 +289,7 @@ def ensure_block_with_lines( def ensure_plan_block( lines: list[str], progress_path: Path, plan_keys: list[str] ) -> tuple[list[str], int, int]: - lines, _, _ = ensure_workflow_state_block(lines) + lines = remove_workflow_state_block(lines) lines, start_idx, end_idx = ensure_block_with_lines( lines, PLAN_STATUS_START, @@ -372,60 +301,26 @@ def ensure_plan_block( 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 remove_workflow_state_block(lines: list[str]) -> list[str]: + block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END) + if not block: + return lines + start_idx, end_idx = block + remove_start = start_idx + if ( + remove_start >= 2 + and lines[remove_start - 1].strip() == "" + and lines[remove_start - 2].strip() == "## Workflow State" + ): + remove_start -= 2 + if remove_start > 0 and lines[remove_start - 1].strip() == "": + remove_start -= 1 -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, - claimed_by: Optional[str] = None, - claimed_at: Optional[str] = None, - verification: Optional[str] = None, - clear_keys: tuple[str, ...] = (), -) -> list[str]: - lines, start_idx, end_idx = ensure_workflow_state_block(lines) - state = parse_workflow_state(lines, start_idx, end_idx) - for key in clear_keys: - state.pop(key, None) - 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 - if claimed_by is not None: - state["claimed_by"] = claimed_by - if claimed_at is not None: - state["claimed_at"] = claimed_at - if verification is not None: - state["verification"] = verification - 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"), - state.get("claimed_by"), - state.get("claimed_at"), - state.get("verification"), - ) + remove_end = end_idx + 1 + if remove_end < len(lines) and lines[remove_end].strip() == "": + remove_end += 1 + del lines[remove_start:remove_end] return lines @@ -450,6 +345,21 @@ def ensure_all_plans_present( return entries +def ensure_plan_status_entry(lines: list[str], plan_key: str) -> list[str]: + lines = remove_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", + ) + entries = parse_entries(lines, start_idx, end_idx) + if plan_key not in {entry_plan for entry_plan, _, _, _ in entries}: + lines[end_idx:end_idx] = [render_plan_line(plan_key, "pending", None)] + return lines + + def filter_existing_entries( entries: list[tuple[str, str, Optional[str], int]], plan_keys: list[str] ) -> list[tuple[str, str, Optional[str], int]]: @@ -483,12 +393,8 @@ def choose_claim_entry( current_env: Optional[str], plan_keys: list[str], ) -> Optional[tuple[str, Optional[str], int]]: - entry_by_plan: dict[str, tuple[str, str, Optional[str], int]] = {} - for entry in entries: - entry_by_plan.setdefault(entry[0], entry) - ordered_entries = [ - entry_by_plan[plan_key] for plan_key in plan_keys if plan_key in entry_by_plan - ] + available = set(plan_keys) + ordered_entries = [entry for entry in entries if entry[0] in available] for plan_key, status, note, idx in ordered_entries: if status == "in-progress": @@ -537,15 +443,12 @@ def claim_plan( return 0, "NOOP: no claimable plans" 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(), - claimed_by=normalize_note(owner) if owner else default_claim_owner(), - claimed_at=now_utc(), - clear_keys=("verification",), + claim_note = ( + f"claimed_by: {normalize_note(owner) if owner else default_claim_owner()}; " + f"claimed_at: {now_utc()}" ) + updated_note = f"{note}; {claim_note}" if note else claim_note + lines[idx] = render_plan_line(plan_key, "in-progress", updated_note) write_progress_lines(progress_path, lines) output = [f"PLAN={(plans_dir / plan_key).as_posix()}"] @@ -582,7 +485,6 @@ def finish_plan( entries = parse_entries(lines, start_idx, end_idx) rendered_note = normalize_note(note) if note else None rendered_verified = normalize_note(verified) if verified else None - verification_clear_keys = () if rendered_verified else ("verification",) if rendered_verified: verified_note = f"verified: {rendered_verified}" rendered_note = ( @@ -591,33 +493,14 @@ def finish_plan( else verified_note ) updated_line = render_plan_line(plan_key, status, rendered_note) - workflow_phase = { - "done": "done", - "blocked": "blocked", - "skipped": "skipped", - }[status] for entry_plan, _, _, idx in entries: if entry_plan == plan_key: lines[idx] = updated_line - lines = update_workflow_state( - lines, - phase=workflow_phase, - plan=f"docs/superpowers/plans/{plan_key}", - verification=rendered_verified, - clear_keys=verification_clear_keys, - ) write_progress_lines(progress_path, lines) return 0, updated_line lines[end_idx:end_idx] = [updated_line] - lines = update_workflow_state( - lines, - phase=workflow_phase, - plan=f"docs/superpowers/plans/{plan_key}", - verification=rendered_verified, - clear_keys=verification_clear_keys, - ) write_progress_lines(progress_path, lines) return 0, updated_line @@ -642,11 +525,6 @@ def status_report(plans_dir: Path, progress_path: Path) -> tuple[int, str]: suffix = f": {note}" if note else "" rows.append(f"PLAN {plan_key} {status}{suffix}") - state: dict[str, str] = {} - workflow_block = find_named_block(lines, WORKFLOW_STATE_START, WORKFLOW_STATE_END) - if workflow_block: - state = parse_workflow_state(lines, *workflow_block) - output = [ "STATUS " f"total={len(plan_keys)} " @@ -656,37 +534,14 @@ def status_report(plans_dir: Path, progress_path: Path) -> tuple[int, str]: f"blocked={counts['blocked']} " f"skipped={counts['skipped']}" ] - current_parts = [ - f"{key}={state[key]}" - for key in ( - "phase", - "plan", - "claimed_by", - "claimed_at", - "verification", - ) - if key in state - ] - if current_parts: - output.append("CURRENT " + " ".join(current_parts)) output.extend(rows) return 0, "\n".join(output) -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}" - +def record_plan(progress_path: Path, plan: str) -> tuple[int, str]: with locked_progress(progress_path): lines = load_progress_lines(progress_path) - lines = update_workflow_state(lines, phase, spec, plan, executor, constraints) + lines = ensure_plan_status_entry(lines, normalize_plan_key(plan)) write_progress_lines(progress_path, lines) return 0, "OK" @@ -700,7 +555,7 @@ def main(argv: list[str]) -> int: return 0 mode = argv[0] - if mode not in {"claim", "finish", "record", "status"}: + if mode not in {"claim", "finish", "status"}: print(f"ERROR: unknown mode: {mode}", file=sys.stderr) print(usage(), file=sys.stderr) return 2 @@ -744,26 +599,6 @@ 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 bb359bfe..a4f6f884 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -50,7 +50,6 @@ def usage() -> str: 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" ) @@ -1532,23 +1531,11 @@ def main(argv: list[str]) -> int: 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, + print( + "ERROR: -record-spec has been removed; spec files are the record.", + file=sys.stderr, ) - if code != 0: - print(message, file=sys.stderr) - return code - print(message) - return 0 + return 2 plan_path = parse_cli_value(argv, "-record-plan") if plan_path is not None: @@ -1556,14 +1543,7 @@ def main(argv: list[str]) -> int: 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", - ) + code, message = MAIN_LOOP.record_plan(Path(progress_path), plan_path) if code != 0: print(message, file=sys.stderr) return code diff --git a/templates/AGENT_RULES.template.md b/templates/AGENT_RULES.template.md index b77412e5..159e71a8 100644 --- a/templates/AGENT_RULES.template.md +++ b/templates/AGENT_RULES.template.md @@ -62,12 +62,11 @@ > `using-superpowers` 是会话启动时判断并加载适用 skill 的入口 skill。 - 规划阶段必须走 `using-superpowers -> brainstorming -> writing-plans` -- `$brainstorming` 产出 `docs/superpowers/specs/*-design.md`;写出 spec 后 - 立即用 `playbook.py -record-spec` 记录 `phase=planning` 与 `spec=` +- `$brainstorming` 产出 `docs/superpowers/specs/*-design.md`;spec 文件本身即为设计留痕 - `$writing-plans` 产出 `docs/superpowers/plans/*.md`;写出 plan 后立即用 - `playbook.py -record-plan` 记录 `plan=`、`executor=executing-plans`、 - `constraints=karpathy-guidelines,.agents,AGENT_RULES` -- spec/plan 产出阶段不单独提交或归档,只做文件落地与状态留痕;如外部 skill + `playbook.py -record-plan` 追加到 `memory-bank/progress.md` 的 + `plan-status` 队列,初始状态为 `pending` +- spec/plan 产出阶段不单独提交或归档,只做文件落地与 Plan 入队;如外部 skill 要求写完后立即提交,以本文件为准,推迟到 Plan 完成后统一处理 - Plan 生命周期由 `main_loop.py` 协调,通过 `memory-bank/progress.md` 留痕 - Plan 执行入口只能是主循环:领取前不得进入 `$executing-plans`,领取后默认用 @@ -118,8 +117,7 @@ - `in-progress`:执行中,用于恢复中断任务 - `done`:已完成 - `blocked`:阻塞,需人工介入或切换环境 -- `skipped`:永久跳过,不再执行,`workflow-state.phase` 也写为 - `skipped` +- `skipped`:永久跳过,不再执行 `skipped` 如需恢复,必须手动改回 `pending`。 @@ -140,7 +138,7 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim \ -owner "<当前session或agent标识>" ``` -该命令在锁保护下完成:自动识别当前环境(windows/linux/darwin)、校验 Plan Meta、优先恢复 `in-progress`、选择第一个可执行 Plan、写入 `claimed_by`/`claimed_at` 并清理上一轮 `verification`。 +该命令在锁保护下完成:自动识别当前环境(windows/linux/darwin)、校验 Plan Meta、优先恢复 `in-progress`、按 `plan-status` 行顺序选择第一个可执行 Plan,并在对应 Plan 行写入 `claimed_by`/`claimed_at`。 stdout 必须包含 `PLAN=`;如为环境恢复,还会附带 `NOTE=env:<环境>:`。 @@ -177,11 +175,6 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py status \ **规划留痕**: ```bash -# brainstorming 完成后 -python {{PLAYBOOK_SCRIPTS}}/playbook.py \ - -record-spec docs/superpowers/specs/-design.md \ - -progress memory-bank/progress.md - # writing-plans 完成后 python {{PLAYBOOK_SCRIPTS}}/playbook.py \ -record-plan docs/superpowers/plans/.md \ @@ -235,7 +228,7 @@ python {{PLAYBOOK_SCRIPTS}}/playbook.py \ - 本轮代码、配置、测试、模板改动 - 当前 Plan 文件(创建、补充、勾选 Task、记录结果等) -- `memory-bank/progress.md` 中本轮 `workflow-state`、`plan-status` 与摘要更新 +- `memory-bank/progress.md` 中本轮 `plan-status` 与摘要更新 - 必要 memory 更新(如 `active-context.md`、`decisions.md`) **归档约束**: @@ -251,8 +244,9 @@ python {{PLAYBOOK_SCRIPTS}}/playbook.py \ - 重要决策记录到 `memory-bank/decisions.md` - 待确认事项在回复中显式列出 -- `workflow-state` 和 `plan-status` 只能通过 - `{{PLAYBOOK_SCRIPTS}}/main_loop.py` 维护 +- `plan-status` 是唯一机器状态源,只能通过 + `{{PLAYBOOK_SCRIPTS}}/main_loop.py` 或 + `{{PLAYBOOK_SCRIPTS}}/playbook.py -record-plan` 维护 - `progress.md` 上半部分是短期状态快照,不是 changelog; 阶段变化或执行结束后整理/替换摘要,不做无限追加 - `active-context.md` 是短期上下文快照,不是长期日志; diff --git a/templates/README.md b/templates/README.md index 2f55e094..4811bbc0 100644 --- a/templates/README.md +++ b/templates/README.md @@ -109,7 +109,7 @@ templates/ 执行规则模板,定义 AI 的工作循环和约束。如需项目私有规则,建议维护 `AGENT_RULES.local.md`;该文件通常由 `[sync_rules]` 首次自动创建,其优先级高于 `AGENT_RULES.md`,且后续不会被 playbook 覆盖。 -计划编排与执行细节统一指向 `docs/superpowers/`、`playbook.py -record-spec/-record-plan` 与 `main_loop.py claim/finish`。 +计划编排与执行细节统一指向 `docs/superpowers/`、`playbook.py -record-plan` 与 `main_loop.py claim/finish`。 ### AGENTS.template.md diff --git a/templates/memory-bank/progress.template.md b/templates/memory-bank/progress.template.md index b3b46de2..f9f432e6 100644 --- a/templates/memory-bank/progress.template.md +++ b/templates/memory-bank/progress.template.md @@ -6,8 +6,7 @@ - 上半部分是短期状态快照,不是 changelog - `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化 - 更新摘要时整理/替换旧摘要,不做无限追加 -- 中间的 workflow-state 块记录当前阶段、spec、plan 与执行约束 -- 下半部分的 plan-status 块由 main_loop.py 维护,是唯一权威状态源 +- 下半部分的 plan-status 块由 main_loop.py 维护,是唯一机器状态源 --> ## Current Focus @@ -31,26 +30,14 @@ 以下示例仅用于说明结构,真实状态由 `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 +- [ ] `2026-05-19-next.md` in-progress: claimed_by: codex; claimed_at: 2026-05-19T08:00:00Z +- [x] `2026-05-20-done.md` done: verified: python -m unittest ``` -## Workflow State - - - - ## Plan Status diff --git a/templates/prompts/coding/close-task.template.md b/templates/prompts/coding/close-task.template.md index 06a2054c..eae7c02c 100644 --- a/templates/prompts/coding/close-task.template.md +++ b/templates/prompts/coding/close-task.template.md @@ -23,7 +23,7 @@ 完成当前 Plan 变更归档/提交;未归档不得声明 Plan 完成 - 未验证内容必须显式说明 - 只写对下一轮仍重要的信息 -- 不手工改写 `workflow-state` 或 `plan-status` 状态块 +- 不手工改写 `plan-status` 状态块 ## 执行步骤 @@ -31,23 +31,18 @@ 2. 核对已运行验证与未运行验证 3. 如本轮来自 `main_loop.py claim`,核对 `main_loop.py finish` 是否已经写回 `plan-status` -4. 如本轮来自 `main_loop.py claim`,核对 `workflow-state.phase` - 是否与当前结果一致 -5. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions` -6. 如本轮来自 `main_loop.py claim` 且结果为 `done`,按项目归档机制只归档 +4. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions` +5. 如本轮来自 `main_loop.py claim` 且结果为 `done`,按项目归档机制只归档 当前 Plan 相关差异 -7. 复核剩余差异是否属于其他 session / 其他 Plan,且未混入本轮交付单元 -8. 输出本轮摘要与下一步 +6. 复核剩余差异是否属于其他 session / 其他 Plan,且未混入本轮交付单元 +7. 输出本轮摘要与下一步 ## 状态留痕复核 - 如本轮来自 `main_loop.py claim`,`main_loop.py finish` 是否已经写回 `plan-status` -- 如本轮来自 `main_loop.py claim`,`workflow-state.phase` 是否与当前结果一致 - 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异 是否已经归档/提交,或是否已说明无当前 Plan 差异 -- 如为代码类执行,`workflow-state` 中是否保留了 - `executor=executing-plans` 与既定 `constraints` ## 输出协议 diff --git a/templates/prompts/coding/update-memory.template.md b/templates/prompts/coding/update-memory.template.md index 5130cb75..826ff644 100644 --- a/templates/prompts/coding/update-memory.template.md +++ b/templates/prompts/coding/update-memory.template.md @@ -35,8 +35,7 @@ ### `memory-bank/progress.md` -- 先读取 `workflow-state`:当前阶段、spec、plan、executor、constraints -- 再读取 `plan-status`:当前 Plan 的机器状态 +- 读取 `plan-status`:Plan 队列与机器状态 - 只更新上半部分的人类摘要,不修改状态块 - 上半部分是短期状态快照,不是长期日志 - `Recent Changes` 只保留最近 3-5 条对恢复上下文有价值的变化 @@ -61,14 +60,14 @@ - 临时聊天内容不要写进去 - 高变化信息放 `active-context`,稳定技术模式放 `system-patterns` - 流程规则或项目私有约束变更写入 `AGENT_RULES.local.md` -- `progress.md` 的状态块只由 `main_loop.py` 维护 -- 摘要应与 `workflow-state` / `plan-status` 保持一致 +- `progress.md` 的 `plan-status` 状态块只由 `main_loop.py` 或 + `playbook.py -record-plan` 维护 +- 摘要应与 `plan-status` 保持一致 - 摘要区保持短期状态快照;长期历史依赖项目归档记录、Plan 文件和 `decisions.md` ## 禁止事项 -- 手工改写 `` - 手工改写 `` - 把临时聊天内容、未验证猜测写进摘要 diff --git a/templates/prompts/coding/verify-change.template.md b/templates/prompts/coding/verify-change.template.md index af83e067..c5056dd1 100644 --- a/templates/prompts/coding/verify-change.template.md +++ b/templates/prompts/coding/verify-change.template.md @@ -23,7 +23,7 @@ - 验证命令必须 fresh run - 局部修改优先局部验证 - 不能运行的验证必须写明原因 -- 不手工改写 `workflow-state` 或 `plan-status` 状态块 +- 不手工改写 `plan-status` 状态块 - 如本轮来自 `main_loop.py claim`,验证通过不等于 Plan 完成;Plan `done` 还必须完成当前 Plan 变更归档/提交 @@ -33,8 +33,7 @@ 2. 运行与本次改动直接相关的验证命令 3. 记录命令、结果和关键输出 4. 复核 diff 是否只包含预期修改 -5. 如本轮来自 `main_loop.py claim`,复核 `workflow-state.phase`、 - `plan-status` 与当前声明一致 +5. 如本轮来自 `main_loop.py claim`,复核 `plan-status` 与当前声明一致 6. 如本轮来自 `main_loop.py claim` 且结果为 `done`,复核当前 Plan 相关差异是否已经归档/提交;未归档时只能声明“验证完成”, 不能声明“Plan 完成” @@ -58,14 +57,10 @@ ## 状态留痕复核 -- 如本轮来自 `main_loop.py claim`,`workflow-state.phase` 是否与当前声明一致 - 如本轮来自 `main_loop.py claim`,`plan-status` 是否已经通过 `main_loop.py finish` 写回 - 如本轮来自 `main_loop.py claim` 且结果为 `done`,当前 Plan 相关差异 是否已经归档/提交,或是否已说明无当前 Plan 差异 -- 如为代码类任务,`workflow-state` 中是否保留: - `executor=executing-plans` - `constraints=karpathy-guidelines,.agents,AGENT_RULES` ## 停止条件 diff --git a/test/cli/test_playbook_cli.py b/test/cli/test_playbook_cli.py index d57b531c..e20d6126 100644 --- a/test/cli/test_playbook_cli.py +++ b/test/cli/test_playbook_cli.py @@ -33,7 +33,7 @@ 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): + def test_record_spec_is_removed(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -47,12 +47,13 @@ class PlaybookCliTests(unittest.TestCase): str(progress), ) - self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) + self.assertEqual(result.returncode, 2) + self.assertIn("-record-spec has been removed", 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.assertNotIn("workflow-state", text) + self.assertNotIn("2026-05-18-demo-design.md", text) - def test_record_plan_updates_progress_workflow_state(self): + def test_record_plan_appends_pending_plan_status(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -62,12 +63,10 @@ class PlaybookCliTests(unittest.TestCase): [ "# 当前进展", "", - "## Workflow State", + "## Plan Status", "", - "", - "phase: planning", - "spec: docs/superpowers/specs/2026-05-18-demo-design.md", - "", + "", + "", ] ) + "\n", @@ -83,13 +82,44 @@ class PlaybookCliTests(unittest.TestCase): 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, + self.assertNotIn("workflow-state", text) + self.assertIn("- [ ] `2026-05-18-demo.md` pending", text) + + def test_record_plan_appends_pending_plan_status_after_existing_entries(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", + "", + "", + "- [ ] `2026-05-18-first.md` pending", + "", + ] + ) + + "\n", + encoding="utf-8", + ) + + result = run_cli( + "-record-plan", + "docs/superpowers/plans/2026-05-18-second.md", + "-progress", + str(progress), + ) + + self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) + text = progress.read_text(encoding="utf-8") + self.assertIn("- [ ] `2026-05-18-first.md` pending", text) + self.assertIn("- [ ] `2026-05-18-second.md` pending", text) + self.assertLess( + text.index("`2026-05-18-first.md`"), + text.index("`2026-05-18-second.md`"), ) def test_missing_config_is_error(self): diff --git a/test/test_main_loop_cli.py b/test/test_main_loop_cli.py index c068d2e3..cd3217a1 100644 --- a/test/test_main_loop_cli.py +++ b/test/test_main_loop_cli.py @@ -11,6 +11,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "main_loop.py" +PLAYBOOK_SCRIPT = ROOT / "scripts" / "playbook.py" _SPEC = importlib.util.spec_from_file_location("playbook_main_loop", SCRIPT) assert _SPEC and _SPEC.loader @@ -82,15 +83,65 @@ class MainLoopCliTests(unittest.TestCase): 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.assertNotIn("workflow-state", text) self.assertIn("", text) self.assertIn("", text) self.assertIn("`2026-01-01-old.md` in-progress", text) + self.assertIn("claimed_by:", text) + self.assertIn("claimed_at:", text) self.assertIn("`2026-01-02-new.md` pending", text) + def test_claim_uses_plan_status_order_before_filename_order(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-alpha.md").write_text( + valid_plan_text("alpha"), encoding="utf-8" + ) + (plans_dir / "2026-01-02-beta.md").write_text( + valid_plan_text("beta"), encoding="utf-8" + ) + + progress = root / "memory-bank" / "progress.md" + progress.parent.mkdir(parents=True) + progress.write_text( + "\n".join( + [ + "# 当前进展", + "", + "## Plan Status", + "", + "", + "- [ ] `2026-01-02-beta.md` pending", + "- [ ] `2026-01-01-alpha.md` pending", + "", + "", + ] + ) + + "\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) + self.assertEqual( + result.stdout.strip(), + "PLAN=docs/superpowers/plans/2026-01-02-beta.md", + ) + + text = progress.read_text(encoding="utf-8") + self.assertIn("- [ ] `2026-01-02-beta.md` in-progress", text) + self.assertIn("- [ ] `2026-01-01-alpha.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) @@ -134,7 +185,7 @@ class MainLoopCliTests(unittest.TestCase): text = progress.read_text(encoding="utf-8") self.assertIn("- keep-this-focus", text) self.assertIn("- keep-this-change", text) - self.assertIn("", text) + self.assertNotIn("workflow-state", text) self.assertIn("", text) self.assertIn("`2026-01-01-demo.md` in-progress", text) @@ -198,10 +249,6 @@ class MainLoopCliTests(unittest.TestCase): [ "# Plan 状态", "", - "", - "phase: planning", - "", - "", "", "- [ ] `2026-01-01-deleted.md` pending", "- [ ] `2026-01-02-live.md` pending", @@ -351,7 +398,7 @@ class MainLoopCliTests(unittest.TestCase): self.assertIn("missing required Plan Meta", result.stderr) self.assertIn("2026-01-01-invalid.md", result.stderr) - def test_claim_records_claim_metadata_in_workflow_state(self): + def test_claim_records_claim_metadata_in_plan_status(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) plans_dir = root / "docs" / "superpowers" / "plans" @@ -375,10 +422,13 @@ class MainLoopCliTests(unittest.TestCase): text = (root / "memory-bank" / "progress.md").read_text( encoding="utf-8" ) - self.assertIn("claimed_by: codex-test", text) + self.assertIn( + "- [ ] `2026-01-01-demo.md` in-progress: claimed_by: codex-test; ", + text, + ) self.assertRegex(text, r"claimed_at: \d{4}-\d{2}-\d{2}T") - def test_claim_clears_stale_verification_from_workflow_state(self): + def test_claim_removes_legacy_workflow_state(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) plans_dir = root / "docs" / "superpowers" / "plans" @@ -420,6 +470,7 @@ class MainLoopCliTests(unittest.TestCase): self.assertEqual(result.returncode, 0, msg=result.stderr) text = progress.read_text(encoding="utf-8") self.assertNotIn("verification: old evidence", text) + self.assertNotIn("workflow-state", text) def test_finish_updates_line(self): with tempfile.TemporaryDirectory() as tmp_dir: @@ -469,11 +520,6 @@ class MainLoopCliTests(unittest.TestCase): [ "# Plan 状态", "", - "", - "phase: executing", - "plan: docs/superpowers/plans/2026-01-03-demo.md", - "", - "", "", "- [ ] `2026-01-03-demo.md` in-progress", "", @@ -503,12 +549,9 @@ class MainLoopCliTests(unittest.TestCase): "verified: python -m unittest tests.test_main_loop_cli", text, ) - self.assertIn( - "verification: python -m unittest tests.test_main_loop_cli", - text, - ) + self.assertNotIn("workflow-state", text) - def test_finish_without_verified_clears_stale_verification(self): + def test_finish_without_verified_removes_legacy_workflow_state(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -549,9 +592,13 @@ class MainLoopCliTests(unittest.TestCase): self.assertEqual(result.returncode, 0, msg=result.stderr) text = progress.read_text(encoding="utf-8") self.assertNotIn("verification: old evidence", text) - self.assertIn("phase: blocked", text) + self.assertNotIn("workflow-state", text) + self.assertIn( + "- [ ] `2026-01-03-demo.md` blocked: needs confirmation", + text, + ) - def test_finish_updates_workflow_phase_and_preserves_metadata(self): + def test_finish_removes_legacy_workflow_state_and_updates_plan_line(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -596,17 +643,11 @@ class MainLoopCliTests(unittest.TestCase): 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, - ) + self.assertNotIn("workflow-state", text) + self.assertNotIn("2026-05-18-demo-design.md", text) + self.assertIn("- [x] `2026-05-18-demo.md` done", text) - def test_finish_skipped_updates_workflow_phase_to_skipped(self): + def test_finish_skipped_updates_plan_line_only(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -650,10 +691,10 @@ class MainLoopCliTests(unittest.TestCase): self.assertEqual(result.returncode, 0, msg=result.stderr) text = progress.read_text(encoding="utf-8") - self.assertIn("phase: skipped", text) + self.assertNotIn("workflow-state", text) self.assertIn("- [ ] `2026-05-18-demo.md` skipped: obsolete", text) - def test_record_updates_workflow_state_block(self): + def test_record_mode_is_removed(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -691,21 +732,12 @@ class MainLoopCliTests(unittest.TestCase): cwd=root, ) - self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertEqual(result.returncode, 2) + self.assertIn("unknown mode: record", 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, - ) + self.assertNotIn("workflow-state", text) - def test_record_claim_finish_workflow_chain(self): + def test_record_plan_claim_finish_queue_chain(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) plans_dir = root / "docs" / "superpowers" / "plans" @@ -717,35 +749,10 @@ class MainLoopCliTests(unittest.TestCase): 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, + code, message = MAIN_LOOP.record_plan( + progress, "docs/superpowers/plans/2026-05-18-demo.md" ) - 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) + self.assertEqual(code, 0, msg=message) result = run_cli( "claim", @@ -770,19 +777,10 @@ class MainLoopCliTests(unittest.TestCase): 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.assertNotIn("workflow-state", text) self.assertIn("- [x] `2026-05-18-demo.md` done", text) - def test_status_reports_counts_and_current_workflow_state(self): + def test_status_reports_counts_from_plan_status(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) plans_dir = root / "docs" / "superpowers" / "plans" @@ -836,14 +834,9 @@ class MainLoopCliTests(unittest.TestCase): "STATUS total=5 pending=1 in-progress=1 done=1 blocked=1 skipped=1", result.stdout, ) - self.assertIn( - "CURRENT phase=executing " - "plan=docs/superpowers/plans/2026-01-02-b.md " - "claimed_by=codex-test", - result.stdout, - ) + self.assertNotIn("CURRENT ", result.stdout) - def test_concurrent_record_preserves_spec_and_plan_metadata(self): + def test_concurrent_record_plan_preserves_queue_entries(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -865,25 +858,17 @@ class MainLoopCliTests(unittest.TestCase): try: threads = [ threading.Thread( - target=MAIN_LOOP.record_workflow_state, + target=MAIN_LOOP.record_plan, args=( progress, - "planning", - "docs/superpowers/specs/2026-05-18-demo-design.md", - None, - None, - None, + "docs/superpowers/plans/2026-05-18-first.md", ), ), threading.Thread( - target=MAIN_LOOP.record_workflow_state, + target=MAIN_LOOP.record_plan, args=( progress, - "planning", - None, - "docs/superpowers/plans/2026-05-18-demo.md", - "executing-plans", - "karpathy-guidelines,.agents,AGENT_RULES", + "docs/superpowers/plans/2026-05-18-second.md", ), ), ] @@ -896,18 +881,11 @@ class MainLoopCliTests(unittest.TestCase): 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, - ) + self.assertNotIn("workflow-state", text) + self.assertIn("- [ ] `2026-05-18-first.md` pending", text) + self.assertIn("- [ ] `2026-05-18-second.md` pending", text) - def test_cross_process_record_lock_preserves_spec_and_plan_metadata(self): + def test_cross_process_record_plan_lock_preserves_queue_entries(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" @@ -919,14 +897,11 @@ class MainLoopCliTests(unittest.TestCase): proc = subprocess.Popen( [ sys.executable, - str(SCRIPT), - "record", + str(PLAYBOOK_SCRIPT), + "-record-plan", + "docs/superpowers/plans/2026-05-18-first.md", "-progress", - "memory-bank/progress.md", - "-phase", - "planning", - "-spec", - "docs/superpowers/specs/2026-05-18-demo-design.md", + str(progress), ], cwd=root, env=slow_env, @@ -936,18 +911,17 @@ class MainLoopCliTests(unittest.TestCase): ) 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", + result = subprocess.run( + [ + sys.executable, + str(PLAYBOOK_SCRIPT), + "-record-plan", + "docs/superpowers/plans/2026-05-18-second.md", + "-progress", + str(progress), + ], + capture_output=True, + text=True, cwd=root, ) @@ -956,16 +930,9 @@ class MainLoopCliTests(unittest.TestCase): 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, - ) + self.assertNotIn("workflow-state", text) + self.assertIn("- [ ] `2026-05-18-first.md` pending", text) + self.assertIn("- [ ] `2026-05-18-second.md` pending", text) if __name__ == "__main__": diff --git a/test/test_sync_templates_placeholders.py b/test/test_sync_templates_placeholders.py index 175caa9f..3eed9e90 100644 --- a/test/test_sync_templates_placeholders.py +++ b/test/test_sync_templates_placeholders.py @@ -85,7 +85,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): update_memory_template = ( ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md" ).read_text(encoding="utf-8") - self.assertIn("workflow-state", update_memory_template) + self.assertNotIn("workflow-state", update_memory_template) self.assertIn("plan-status", update_memory_template) self.assertIn("## Updated Files", update_memory_template) self.assertIn("## New Context", update_memory_template) @@ -106,7 +106,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): ).read_text(encoding="utf-8") self.assertIn("main_loop.py finish", close_task_template) self.assertIn("如本轮来自 `main_loop.py claim`", close_task_template) - self.assertIn("workflow-state.phase", close_task_template) + self.assertNotIn("workflow-state", close_task_template) self.assertIn("未归档不得声明 Plan 完成", close_task_template) self.assertIn("按项目归档机制只归档", close_task_template) self.assertIn("状态已写回,交付未完成", close_task_template) @@ -120,7 +120,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md" ).read_text(encoding="utf-8") self.assertIn("如本轮来自 `main_loop.py claim`", verify_change_template) - self.assertIn("workflow-state.phase", verify_change_template) + self.assertNotIn("workflow-state", verify_change_template) self.assertIn("plan-status", verify_change_template) self.assertIn("验证通过不等于 Plan 完成", verify_change_template) self.assertIn( @@ -269,9 +269,9 @@ langs = [\"cpp\", \"tsl\"] ) self.assertIn("docs/superpowers/plans", rules_text) self.assertNotIn("plan_progress.py", rules_text) - self.assertIn("记录 `phase=planning` 与 `spec=`", rules_text) + self.assertNotIn("record-spec", rules_text) self.assertIn( - "记录 `plan=`、`executor=executing-plans`、", + "追加到 `memory-bank/progress.md` 的", rules_text, ) self.assertIn("领取前不得进入 `$executing-plans`", rules_text) @@ -356,10 +356,9 @@ project_name = "MyProject" 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.assertNotIn("phase: planning", progress_text) + self.assertNotIn("executor: executing-plans", progress_text) + self.assertNotIn("workflow-state", progress_text) self.assertIn("## Plan Status", progress_text) self.assertIn("", progress_text) self.assertIn("", progress_text)