🐛 fix(main_loop): enforce plan lifecycle ordering

Claim the first executable plan in plan-file order after resuming any in-progress plan, preserve skipped as its own workflow phase, and document the required post-done commit gate in the agent rules.
This commit is contained in:
csh 2026-05-27 14:07:09 +08:00
parent 61afff9d48
commit 1a3ea1f425
6 changed files with 221 additions and 48 deletions

View File

@ -31,7 +31,14 @@ PLAN_LINE_RE = re.compile(
) )
FINISH_STATUSES = {"done", "blocked", "skipped"} FINISH_STATUSES = {"done", "blocked", "skipped"}
ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$") ENV_BLOCKED_RE = re.compile(r"^env:([^:]+):(.+)$")
WORKFLOW_PHASES = {"brainstorming", "planning", "executing", "done", "blocked"} 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()
@ -51,7 +58,7 @@ def usage() -> str:
" -plan PATH\n" " -plan PATH\n"
" -status done|blocked|skipped\n" " -status done|blocked|skipped\n"
" -progress FILE\n" " -progress FILE\n"
" -phase brainstorming|planning|executing|done|blocked\n" " -phase brainstorming|planning|executing|done|blocked|skipped\n"
" -spec PATH\n" " -spec PATH\n"
" -executor NAME\n" " -executor NAME\n"
" -constraints CSV\n" " -constraints CSV\n"
@ -365,13 +372,19 @@ def update_workflow_state(
def ensure_all_plans_present( def ensure_all_plans_present(
lines: list[str], start_idx: int, end_idx: int, progress_path: Path, plan_keys: list[str] lines: list[str],
start_idx: int,
end_idx: int,
progress_path: Path,
plan_keys: list[str],
) -> list[tuple[str, str, Optional[str], int]]: ) -> list[tuple[str, str, Optional[str], int]]:
entries = parse_entries(lines, start_idx, end_idx) entries = parse_entries(lines, start_idx, end_idx)
existing = {plan_key for plan_key, _, _, _ in entries} existing = {plan_key for plan_key, _, _, _ in entries}
missing = [plan_key for plan_key in plan_keys if plan_key not in existing] missing = [plan_key for plan_key in plan_keys if plan_key not in existing]
if missing: if missing:
insert_lines = [render_plan_line(plan_key, "pending", None) for plan_key in missing] insert_lines = [
render_plan_line(plan_key, "pending", None) for plan_key in missing
]
lines[end_idx:end_idx] = insert_lines lines[end_idx:end_idx] = insert_lines
write_progress_lines(progress_path, lines) write_progress_lines(progress_path, lines)
end_idx += len(insert_lines) end_idx += len(insert_lines)
@ -387,23 +400,29 @@ def filter_existing_entries(
def choose_claim_entry( def choose_claim_entry(
entries: list[tuple[str, str, Optional[str], int]], current_env: Optional[str] entries: list[tuple[str, str, Optional[str], int]],
current_env: Optional[str],
plan_keys: list[str],
) -> Optional[tuple[str, Optional[str], int]]: ) -> Optional[tuple[str, Optional[str], int]]:
for plan_key, status, note, idx in entries: 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
]
for plan_key, status, note, idx in ordered_entries:
if status == "in-progress": if status == "in-progress":
return plan_key, note, idx return plan_key, note, idx
for plan_key, status, note, idx in entries: for plan_key, status, note, idx in ordered_entries:
if status == "pending": if status == "pending":
return plan_key, note, idx return plan_key, note, idx
if status != "blocked" or not current_env:
if current_env: continue
for plan_key, status, note, idx in entries: env_info = parse_env_blocked_note(note)
if status != "blocked": if env_info and env_info[0] == current_env:
continue return plan_key, note, idx
env_info = parse_env_blocked_note(note)
if env_info and env_info[0] == current_env:
return plan_key, note, idx
return None return None
@ -419,13 +438,17 @@ def claim_plan(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
with locked_progress(progress_path): with locked_progress(progress_path):
lines = load_progress_lines(progress_path) lines = load_progress_lines(progress_path)
try: try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, plan_keys) lines, start_idx, end_idx = ensure_plan_block(
lines, progress_path, plan_keys
)
except ValueError as exc: except ValueError as exc:
return 2, f"ERROR: {exc}" return 2, f"ERROR: {exc}"
entries = ensure_all_plans_present(lines, start_idx, end_idx, progress_path, plan_keys) entries = ensure_all_plans_present(
lines, start_idx, end_idx, progress_path, plan_keys
)
entries = filter_existing_entries(entries, plan_keys) entries = filter_existing_entries(entries, plan_keys)
chosen = choose_claim_entry(entries, detect_env()) chosen = choose_claim_entry(entries, detect_env(), plan_keys)
if not chosen: if not chosen:
return 0, "NOOP: no claimable plans" return 0, "NOOP: no claimable plans"
@ -457,18 +480,24 @@ def finish_plan(
lines = load_progress_lines(progress_path) lines = load_progress_lines(progress_path)
try: try:
lines, start_idx, end_idx = ensure_plan_block(lines, progress_path, [plan_key]) lines, start_idx, end_idx = ensure_plan_block(
lines, progress_path, [plan_key]
)
except ValueError as exc: except ValueError as exc:
return 2, f"ERROR: {exc}" return 2, f"ERROR: {exc}"
entries = parse_entries(lines, start_idx, end_idx) entries = parse_entries(lines, start_idx, end_idx)
rendered_note = normalize_note(note) if note else None rendered_note = normalize_note(note) if note else None
updated_line = render_plan_line(plan_key, status, rendered_note) updated_line = render_plan_line(plan_key, status, rendered_note)
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
workflow_phase = "done" if status == "done" else "blocked"
lines = update_workflow_state( lines = update_workflow_state(
lines, lines,
phase=workflow_phase, phase=workflow_phase,
@ -478,7 +507,6 @@ def finish_plan(
return 0, updated_line return 0, updated_line
lines[end_idx:end_idx] = [updated_line] lines[end_idx:end_idx] = [updated_line]
workflow_phase = "done" if status == "done" else "blocked"
lines = update_workflow_state( lines = update_workflow_state(
lines, lines,
phase=workflow_phase, phase=workflow_phase,

View File

@ -83,7 +83,7 @@
- `$subagent-driven-development` 仅在 Plan 或平台明确要求时使用, - `$subagent-driven-development` 仅在 Plan 或平台明确要求时使用,
不是默认执行器 不是默认执行器
- 执行完成后,必须先运行 `main_loop.py finish` 写回状态, - 执行完成后,必须先运行 `main_loop.py finish` 写回状态,
再更新 `progress.md` 上半部分摘要 再更新 `progress.md` 上半部分摘要,并按主循环收尾要求提交
### Plan 要求 ### Plan 要求
@ -116,7 +116,8 @@
- `in-progress`:执行中,用于恢复中断任务 - `in-progress`:执行中,用于恢复中断任务
- `done`:已完成 - `done`:已完成
- `blocked`:阻塞,需人工介入或切换环境 - `blocked`:阻塞,需人工介入或切换环境
- `skipped`:永久跳过,不再执行 - `skipped`:永久跳过,不再执行,`workflow-state.phase` 也写为
`skipped`
`skipped` 如需恢复,必须手动改回 `pending` `skipped` 如需恢复,必须手动改回 `pending`
@ -139,8 +140,9 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py claim \
该命令会在锁保护下串行完成三件事: 该命令会在锁保护下串行完成三件事:
- 自动识别当前环境:`windows`、`linux`、`darwin` - 自动识别当前环境:`windows`、`linux`、`darwin`
- 按顺序选择可执行 Plan - 已有 `in-progress` 优先恢复
`in-progress` > `pending` > `blocked: env:<当前环境>:...` - 如无 `in-progress`,按 Plan 文件顺序选择第一个可执行 Plan
`pending``blocked: env:<当前环境>:...`
- 将选中的 Plan 写成 `in-progress` - 将选中的 Plan 写成 `in-progress`
这里的锁保护的是 `progress.md` 状态块更新,避免多个 session 这里的锁保护的是 `progress.md` 状态块更新,避免多个 session
@ -218,10 +220,27 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
8. 触碰安全红线时立即停止,不继续后续 Plan 8. 触碰安全红线时立即停止,不继续后续 Plan
9. 常规模式下可对高风险事项向用户确认;无交互模式按本文件 9. 常规模式下可对高风险事项向用户确认;无交互模式按本文件
的“需要确认的场景”自动处理 的“需要确认的场景”自动处理
10. 每次 `claim` 只领取一个 Plan写回后再领取下一个 10. 每次 `claim` 只领取一个 Plan写回并完成必要提交后再领取下一个
11. 全部 Plan 处理完后,统一汇总完成项、阻塞项、跳过项、 11. 全部 Plan 处理完后,统一汇总完成项、阻塞项、跳过项、
环境需求与待确认事项 环境需求与待确认事项
### Plan 完成提交契约
- Plan `done` 后必须完成一次 commit然后才能继续领取下一个 Plan
- 提交顺序:
1. 完成 Plan 约定验证
2. 运行 `main_loop.py finish -status done` 写回状态
3. 必要时更新 `progress.md` 上半部分摘要和相关 memory
4. 检查 `git status --short` 与 diff
5. 只提交当前 Plan 相关改动
- 不得由 `main_loop.py finish` 自动执行 `git commit`
- 不得把用户已有改动或其他 Plan 的改动混入当前 Plan commit
- 如 Plan `done` 后没有 diff必须在回复中说明无提交原因
- `blocked` / `skipped` 不默认提交代码改动;只有状态留痕或已验证的
局部成果需要保留时才提交
- 工作区保持干净后再领取下一个 Plan如存在不属于当前 Plan 的脏改动,
常规模式先向用户确认,无交互模式写入风险并停止继续领取
## 通用执行约束 ## 通用执行约束
### 代码与配置修改 ### 代码与配置修改
@ -297,6 +316,7 @@ python {{PLAYBOOK_SCRIPTS}}/main_loop.py finish \
- [ ] 换行符与文件格式正确 - [ ] 换行符与文件格式正确
- [ ] 无语法错误或明显运行时错误 - [ ] 无语法错误或明显运行时错误
- [ ] 已通过 `main_loop.py finish` 写回 Plan 状态 - [ ] 已通过 `main_loop.py finish` 写回 Plan 状态
- [ ] Plan `done` 后已完成对应 commit或已说明无 diff 无需提交
--- ---

View File

@ -17,7 +17,8 @@
## 规则 ## 规则
- 如果任务状态变更,优先通过 `main_loop.py finish` 留痕 - 如本轮来自 `main_loop.py claim` 且任务状态变更,优先通过
`main_loop.py finish` 留痕
- 未验证内容必须显式说明 - 未验证内容必须显式说明
- 只写对下一轮仍重要的信息 - 只写对下一轮仍重要的信息
- 不手工改写 `workflow-state``plan-status` 状态块 - 不手工改写 `workflow-state``plan-status` 状态块
@ -26,15 +27,18 @@
1. 核对已完成项与未完成项 1. 核对已完成项与未完成项
2. 核对已运行验证与未运行验证 2. 核对已运行验证与未运行验证
3. 核对 `main_loop.py finish` 是否已经写回 `plan-status` 3. 如本轮来自 `main_loop.py claim`,核对 `main_loop.py finish`
4. 核对 `workflow-state.phase` 是否与当前结果一致 是否已经写回 `plan-status`
4. 如本轮来自 `main_loop.py claim`,核对 `workflow-state.phase`
是否与当前结果一致
5. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions` 5. 如需回写上下文,更新 `active-context`、`progress` 上半部分和 `decisions`
6. 输出本轮摘要与下一步 6. 输出本轮摘要与下一步
## 状态留痕复核 ## 状态留痕复核
- `main_loop.py finish` 是否已经写回 `plan-status` - 如本轮来自 `main_loop.py claim``main_loop.py finish` 是否已经写回
- `workflow-state.phase` 是否与当前结果一致 `plan-status`
- 如本轮来自 `main_loop.py claim``workflow-state.phase` 是否与当前结果一致
- 如为代码类执行,`workflow-state` 中是否保留了 - 如为代码类执行,`workflow-state` 中是否保留了
`executor=executing-plans` 与既定 `constraints` `executor=executing-plans` 与既定 `constraints`
@ -59,7 +63,7 @@
## 停止条件 ## 停止条件
- 如状态未写回,先完成留痕再收尾 - 如已领取 Plan 但状态未写回,先完成留痕再收尾
- 如验证不足以支持交付,停止并标记风险 - 如验证不足以支持交付,停止并标记风险
--- ---

View File

@ -31,7 +31,8 @@
2. 运行与本次改动直接相关的验证命令 2. 运行与本次改动直接相关的验证命令
3. 记录命令、结果和关键输出 3. 记录命令、结果和关键输出
4. 复核 diff 是否只包含预期修改 4. 复核 diff 是否只包含预期修改
5. 复核 `workflow-state.phase`、`plan-status` 与当前声明一致 5. 如本轮来自 `main_loop.py claim`,复核 `workflow-state.phase`
`plan-status` 与当前声明一致
6. 汇总未覆盖项和剩余风险 6. 汇总未覆盖项和剩余风险
## 输出协议 ## 输出协议
@ -52,8 +53,9 @@
## 状态留痕复核 ## 状态留痕复核
- `workflow-state.phase` 是否与当前声明一致 - 如本轮来自 `main_loop.py claim``workflow-state.phase` 是否与当前声明一致
- `plan-status` 是否已经通过 `main_loop.py finish` 写回 - 如本轮来自 `main_loop.py claim``plan-status` 是否已经通过
`main_loop.py finish` 写回
- 如为代码类任务,`workflow-state` 中是否保留: - 如为代码类任务,`workflow-state` 中是否保留:
`executor=executing-plans` `executor=executing-plans`
`constraints=karpathy-guidelines,.agents,AGENT_RULES` `constraints=karpathy-guidelines,.agents,AGENT_RULES`

View File

@ -245,6 +245,55 @@ class MainLoopCliTests(unittest.TestCase):
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn(f"`2026-01-05-env.md` in-progress: {note}", text) self.assertIn(f"`2026-01-05-env.md` in-progress: {note}", text)
def test_claim_prefers_earlier_env_blocked_plan_over_later_pending_plan(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-env.md").write_text("env", encoding="utf-8")
(plans_dir / "2026-01-02-pending.md").write_text(
"pending", encoding="utf-8"
)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
env = self._current_env()
note = f"env:{env}:Task2"
progress.write_text(
"\n".join(
[
"# Plan 状态",
"",
"<!-- plan-status:start -->",
f"- [ ] `2026-01-01-env.md` blocked: {note}",
"- [ ] `2026-01-02-pending.md` pending",
"<!-- plan-status:end -->",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"claim",
"-plans",
"docs/superpowers/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(
result.stdout.strip(),
"\n".join(
[
"PLAN=docs/superpowers/plans/2026-01-01-env.md",
f"NOTE={note}",
]
),
)
def test_finish_updates_line(self): def test_finish_updates_line(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
@ -329,13 +378,62 @@ 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.assertIn("phase: done", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text) self.assertIn(
"spec: docs/superpowers/specs/2026-05-18-demo-design.md", text
)
self.assertIn("executor: executing-plans", text) self.assertIn("executor: executing-plans", text)
self.assertIn( self.assertIn(
"constraints: karpathy-guidelines,.agents,AGENT_RULES", "constraints: karpathy-guidelines,.agents,AGENT_RULES",
text, text,
) )
def test_finish_skipped_updates_workflow_phase_to_skipped(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"# 当前进展",
"",
"## Workflow State",
"",
"<!-- workflow-state:start -->",
"phase: executing",
"plan: docs/superpowers/plans/2026-05-18-demo.md",
"<!-- workflow-state:end -->",
"",
"## Plan Status",
"",
"<!-- plan-status:start -->",
"- [ ] `2026-05-18-demo.md` in-progress",
"<!-- plan-status:end -->",
"",
]
)
+ "\n",
encoding="utf-8",
)
result = run_cli(
"finish",
"-plan",
"docs/superpowers/plans/2026-05-18-demo.md",
"-status",
"skipped",
"-progress",
"memory-bank/progress.md",
"-note",
"obsolete",
cwd=root,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
text = progress.read_text(encoding="utf-8")
self.assertIn("phase: skipped", text)
self.assertIn("- [ ] `2026-05-18-demo.md` skipped: obsolete", text)
def test_record_updates_workflow_state_block(self): def test_record_updates_workflow_state_block(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
@ -378,7 +476,9 @@ class MainLoopCliTests(unittest.TestCase):
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn("<!-- workflow-state:start -->", text) self.assertIn("<!-- workflow-state:start -->", text)
self.assertIn("phase: planning", text) self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", 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("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text) self.assertIn("executor: executing-plans", text)
self.assertIn( self.assertIn(
@ -450,7 +550,9 @@ class MainLoopCliTests(unittest.TestCase):
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn("phase: done", text) self.assertIn("phase: done", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", 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("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text) self.assertIn("executor: executing-plans", text)
self.assertIn( self.assertIn(
@ -513,7 +615,9 @@ class MainLoopCliTests(unittest.TestCase):
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text) self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", 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("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text) self.assertIn("executor: executing-plans", text)
self.assertIn( self.assertIn(
@ -571,7 +675,9 @@ class MainLoopCliTests(unittest.TestCase):
text = progress.read_text(encoding="utf-8") text = progress.read_text(encoding="utf-8")
self.assertIn("phase: planning", text) self.assertIn("phase: planning", text)
self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", 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("plan: docs/superpowers/plans/2026-05-18-demo.md", text)
self.assertIn("executor: executing-plans", text) self.assertIn("executor: executing-plans", text)
self.assertIn( self.assertIn(

View File

@ -76,6 +76,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
ROOT / "templates" / "prompts" / "coding" / "close-task.template.md" ROOT / "templates" / "prompts" / "coding" / "close-task.template.md"
).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("workflow-state.phase", close_task_template) self.assertIn("workflow-state.phase", close_task_template)
self.assertIn("## Completed", close_task_template) self.assertIn("## Completed", close_task_template)
self.assertIn("## Not Completed", close_task_template) self.assertIn("## Not Completed", close_task_template)
@ -86,6 +87,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
verify_change_template = ( verify_change_template = (
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("workflow-state.phase", verify_change_template) self.assertIn("workflow-state.phase", verify_change_template)
self.assertIn("plan-status", verify_change_template) self.assertIn("plan-status", verify_change_template)
self.assertIn("## Validated", verify_change_template) self.assertIn("## Validated", verify_change_template)
@ -108,9 +110,9 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
self.assertIn("## Open Questions", code_review_template) self.assertIn("## Open Questions", code_review_template)
self.assertIn("## Residual Risk", code_review_template) self.assertIn("## Residual Risk", code_review_template)
prompts_readme = ( prompts_readme = (ROOT / "templates" / "prompts" / "README.md").read_text(
ROOT / "templates" / "prompts" / "README.md" encoding="utf-8"
).read_text(encoding="utf-8") )
self.assertIn("AGENT_RULES.md", prompts_readme) self.assertIn("AGENT_RULES.md", prompts_readme)
self.assertIn("docs/superpowers/", prompts_readme) self.assertIn("docs/superpowers/", prompts_readme)
self.assertIn("不是流程权威", prompts_readme) self.assertIn("不是流程权威", prompts_readme)
@ -132,16 +134,27 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
self.assertNotIn("main_loop.py claim", agent_behavior_template) self.assertNotIn("main_loop.py claim", agent_behavior_template)
self.assertNotIn("playbook.py -record-spec", agent_behavior_template) self.assertNotIn("playbook.py -record-spec", agent_behavior_template)
self.assertNotIn("playbook.py -record-plan", agent_behavior_template) self.assertNotIn("playbook.py -record-plan", agent_behavior_template)
self.assertIn("项目上下文与执行状态写入 `memory-bank/`", agent_behavior_template) self.assertIn(
"项目上下文与执行状态写入 `memory-bank/`", agent_behavior_template
)
rules_template = ( rules_template = (ROOT / "templates" / "AGENT_RULES.template.md").read_text(
ROOT / "templates" / "AGENT_RULES.template.md" encoding="utf-8"
).read_text(encoding="utf-8") )
self.assertIn("唯一流程约束中心", rules_template) self.assertIn("唯一流程约束中心", rules_template)
self.assertIn("唯一设计与计划产物中心", rules_template) self.assertIn("唯一设计与计划产物中心", rules_template)
self.assertIn("memory-bank/progress.md", rules_template) self.assertIn("memory-bank/progress.md", rules_template)
self.assertIn("已有 `in-progress` 优先恢复", rules_template)
self.assertIn("按 Plan 文件顺序选择第一个可执行 Plan", rules_template)
self.assertIn("Plan `done` 后必须完成一次 commit", rules_template)
self.assertIn(
"不得由 `main_loop.py finish` 自动执行 `git commit`", rules_template
)
self.assertIn("工作区保持干净后再领取下一个 Plan", rules_template)
def test_sync_templates_replaces_playbook_scripts_without_main_language_support(self): def test_sync_templates_replaces_playbook_scripts_without_main_language_support(
self,
):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f""" config_body = f"""
[playbook] [playbook]