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