Files
playbook/test/test_template_contracts.py
T

746 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import importlib.util
import re
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATES = ROOT / "templates"
MAIN_LOOP_ROOT = ROOT / "skills" / "cook-it-through"
MAIN_LOOP_SKILL = MAIN_LOOP_ROOT / "SKILL.md"
MAIN_LOOP_SESSION_BOUNDARY = MAIN_LOOP_ROOT / "rules" / "session-boundary.md"
MAIN_LOOP_WORKFLOWS = {
name: MAIN_LOOP_ROOT / "workflows" / f"{name}.md"
for name in (
"single-session",
"feature-planning",
"ticket-execution",
"feature-integration",
)
}
MAIN_LOOP_SCRIPTS = MAIN_LOOP_ROOT / "scripts"
MAIN_LOOP_SCRIPT = MAIN_LOOP_SCRIPTS / "main_loop.py"
_MAIN_LOOP_SPEC = importlib.util.spec_from_file_location(
"playbook_main_loop_contracts", MAIN_LOOP_SCRIPT
)
assert _MAIN_LOOP_SPEC and _MAIN_LOOP_SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(_MAIN_LOOP_SPEC)
sys.modules[_MAIN_LOOP_SPEC.name] = MAIN_LOOP
_MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
def subcommand_parsers() -> dict[str, argparse.ArgumentParser]:
parser = MAIN_LOOP.build_parser()
for action in parser._subparsers._group_actions: # noqa: SLF001
if isinstance(action, argparse._SubParsersAction): # noqa: SLF001
return dict(action.choices)
raise AssertionError("main_loop.py exposes no subcommands")
def required_flags(parser: argparse.ArgumentParser) -> set[str]:
return {
option
for action in parser._actions # noqa: SLF001
if action.required
for option in action.option_strings
if option.startswith("--")
}
def option_action(
parser: argparse.ArgumentParser, option: str
) -> argparse.Action:
for action in parser._actions: # noqa: SLF001
if option in action.option_strings:
return action
raise AssertionError(f"{parser.prog} exposes no {option}")
def normalized_prose(text: str) -> str:
return " ".join(text.split())
def isolation_choices() -> set[str]:
claim = subcommand_parsers()["claim"]
for action in claim._actions: # noqa: SLF001
if "--isolation" in action.option_strings:
return set(action.choices or ())
raise AssertionError("claim exposes no --isolation choices")
def installed_skills() -> set[str]:
names: set[str] = set()
for base in (ROOT / "skills", ROOT / "skills" / "thirdparty"):
if not base.is_dir():
continue
for path in base.iterdir():
if path.is_dir() and (path / "SKILL.md").is_file():
names.add(path.name)
return names
def rules_text() -> str:
return (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
def main_loop_skill_text() -> str:
return MAIN_LOOP_SKILL.read_text(encoding="utf-8")
def main_loop_session_boundary_text() -> str:
return MAIN_LOOP_SESSION_BOUNDARY.read_text(encoding="utf-8")
def main_loop_workflow_text(name: str) -> str:
return MAIN_LOOP_WORKFLOWS[name].read_text(encoding="utf-8")
def main_loop_instruction_paths() -> tuple[Path, ...]:
return (
MAIN_LOOP_SKILL,
MAIN_LOOP_SESSION_BOUNDARY,
*MAIN_LOOP_WORKFLOWS.values(),
)
def main_loop_bundle_text() -> str:
return "\n".join(
path.read_text(encoding="utf-8") for path in main_loop_instruction_paths()
)
def section(text: str, heading: str, until: str) -> str:
return text.split(heading, 1)[1].split(until, 1)[0]
def headings(text: str) -> list[str]:
return re.findall(r"^#{2,3} (.+)$", text, re.MULTILINE)
# Tokens written in backticks that name something other than a skill. Machine
# state, CLI vocabulary and harness commands are derived from the implementation
# where possible so the allowlist stays small and reviewable.
NON_SKILL_BACKTICK_TOKENS = frozenset(
{
"base",
"claude",
"clear",
"codex",
"command",
"commit",
"compact",
"main",
"pass",
"pass-with-notes",
"passed",
"output",
"result",
"released",
"report",
"spec",
"standards",
}
)
LEGACY_FLOW_TERMS = (
"using-superpowers",
"brainstorming",
"writing-plans",
"executing-plans",
"docs/superpowers/",
"plan-status",
"旧开发流程目录",
"## 禁止旧流程",
"旧开发队列",
"单文件开发计划主链",
"active-context.md",
"progress.md",
"decisions.md",
)
class TemplateContractsTests(unittest.TestCase):
def test_templates_define_only_the_matt_ticket_workflow(self):
combined = "\n".join(
[
*(path.read_text(encoding="utf-8") for path in sorted(TEMPLATES.rglob("*.md"))),
main_loop_bundle_text(),
]
)
for required in (
"setup-matt-pocock-skills",
"grill-with-docs",
"to-spec",
"to-tickets",
".scratch/",
"main_loop.py claim",
"main_loop.py integrate",
):
self.assertIn(required, combined)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, combined)
def test_project_templates_drop_legacy_language_placeholders(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", templates_readme)
self.assertNotIn("{{LANGUAGE_1}}", templates_readme)
self.assertNotIn("docs/workflows/", templates_readme)
self.assertNotIn("templates/workflows/", templates_readme)
self.assertNotIn("docs/superpowers/", templates_readme)
self.assertNotIn(".scratch/<feature>/spec.md", templates_readme)
self.assertIn("`.scratch/<feature>/spec.md`", main_loop_bundle_text())
self.assertNotIn("docs/prompts/", templates_readme)
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
self.assertIn("AGENT_RULES.md", agents_template)
self.assertIn("memory-bank/project-brief.md", agents_template)
self.assertIn("memory-bank/tech-context.md", agents_template)
self.assertIn("memory-bank/system-patterns.md", agents_template)
self.assertIn(".scratch/queue.md", agents_template)
self.assertNotIn("docs/prompts/", agents_template)
tech_context_template = (
TEMPLATES / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_context_template)
self.assertIn("## 不可假设项", tech_context_template)
project_brief_template = (
TEMPLATES / "memory-bank" / "project-brief.template.md"
).read_text(encoding="utf-8")
self.assertIn("## 成功定义", project_brief_template)
def test_agents_template_nests_agents_block_inside_framework_block(self):
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
lines = [line.strip() for line in agents_template.splitlines()]
for marker in (
"<!-- playbook:framework:start -->",
"<!-- playbook:framework:end -->",
"<!-- playbook:agents:start -->",
"<!-- playbook:agents:end -->",
):
self.assertIn(marker, lines, msg=f"missing marker: {marker}")
framework_start = lines.index("<!-- playbook:framework:start -->")
framework_end = lines.index("<!-- playbook:framework:end -->")
agents_start = lines.index("<!-- playbook:agents:start -->")
agents_end = lines.index("<!-- playbook:agents:end -->")
nesting_reason = (
"sync_agents_template replaces the whole framework block; the agents "
"sub-block must stay inside it so preserve_agents_subblock can carry "
"the deployed language rules across the replacement"
)
self.assertLess(framework_start, agents_start, msg=nesting_reason)
self.assertLess(agents_start, agents_end, msg=nesting_reason)
self.assertLess(agents_end, framework_end, msg=nesting_reason)
def test_templates_readme_separates_ownership_from_deployment_config(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
classification = section(templates_readme, "## 模板分类", "## 模板说明")
categories = [h for h in headings(classification) if h[:2] in ("1.", "2.", "3.")]
self.assertEqual(len(categories), 3, msg=f"unexpected categories: {categories}")
self.assertIn("项目新增的 `memory-bank/*`", classification)
self.assertIn(
"playbook:rules:start/end",
classification,
msg="ownership of AGENT_RULES.md is split by the marker block; the "
"classification has to say which side the project owns",
)
self.assertNotIn("[sync_", classification)
self.assertNotIn("force", classification)
self.assertNotIn("no_backup", classification)
def test_templates_readme_separates_state_source_and_protocol_authority(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
normalized = normalized_prose(templates_readme)
self.assertIn("`.scratch/` 是唯一机器状态源", normalized)
self.assertIn(
"由第一方 `cook-it-through` Skill 权威定义",
normalized,
)
self.assertNotIn("preserve_agents_subblock()", templates_readme)
self.assertNotIn("四个入口按成本递增", templates_readme)
self.assertIsNone(
re.search(r"\*\*最后更新\*\*\d{4}-\d{2}-\d{2}", templates_readme),
msg="templates README must not carry a hand-maintained update date",
)
def test_templates_readme_documents_skill_exclusion_boundary(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
deployment = normalized_prose(
section(templates_readme, "## 部署", "## 正式开发流程")
)
layout = normalized_prose(
section(
templates_readme,
"## `playbook.py` 部署后结构",
"## 正式流程运行后按需产生的结构",
)
)
self.assertIn("启用了 `[sync_rules]`", deployment)
self.assertIn("安装集合必须包含 `cook-it-through`", deployment)
self.assertIn('`mode = "all"` 时不得通过 `exclude` 排除', deployment)
self.assertIn(
"只有不部署官方 `AGENT_RULES.md` 且不使用正式工程主链的安装场景,才可以排除该 skill",
deployment,
)
self.assertIn("同时启用 `[sync_rules]` 时,必须遵守上文", layout)
def test_memory_bank_contains_only_stable_project_knowledge(self):
memory_templates = {
path.name for path in (TEMPLATES / "memory-bank").glob("*.template.md")
}
self.assertEqual(
memory_templates,
{
"project-brief.template.md",
"tech-context.template.md",
"system-patterns.template.md",
},
)
boundary = normalized_prose(main_loop_session_boundary_text())
planning = main_loop_workflow_text("feature-planning")
execution = main_loop_workflow_text("ticket-execution")
for path in (
"memory-bank/project-brief.md",
"memory-bank/tech-context.md",
"memory-bank/system-patterns.md",
):
self.assertIn(path, boundary)
for required in (
"已经验证且可复现",
"重新发现成本高",
"不能从代码直接看出",
"下一 session 仍需要",
"关键取舍及理由写入 `docs/adr/`",
"`handoff` 产物写入 OS 临时目录",
):
self.assertIn(required, boundary)
self.assertIn("进入 `grill-with-docs` 前", planning)
self.assertIn("领取后实现前", execution)
def test_prompt_templates_are_not_part_of_the_workflow(self):
self.assertFalse(TEMPLATES.joinpath("prompts").exists())
def test_agent_rules_routes_main_loop_work_to_the_firstparty_skill(self):
rules = rules_text()
normalized = normalized_prose(rules)
self.assertIn("{{PLAYBOOK_ROOT}}", rules)
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules)
self.assertEqual(
headings(rules),
["优先级", "沟通", "项目边界", "工作流入口"],
msg="always-loaded rules must remain a thin workflow bootstrap",
)
self.assertIn("必须加载 `cook-it-through`", rules)
self.assertIn(
"或读取/修改 `.scratch` 中的 queue、ticket、heartbeat、integration 状态前",
normalized,
)
self.assertIn("该 skill 独占", rules)
self.assertIn("主循环执行引擎随该 skill 安装", normalized)
self.assertIn("`.agents/index.md`", rules)
self.assertNotIn("**Blocked by:**", rules)
self.assertNotIn("## 主循环命令", rules)
self.assertLessEqual(len(rules.splitlines()), 50)
self.assertLessEqual(len(rules.encode("utf-8")), 5_000)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, rules)
def test_cook_it_through_skill_owns_the_ticket_contract(self):
rules = rules_text()
bundle = main_loop_bundle_text()
planning = main_loop_workflow_text("feature-planning")
readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
skills_readme = (ROOT / "skills/README.md").read_text(encoding="utf-8")
self.assertFalse((ROOT / "docs/common/main-loop-ticket-contract.md").exists())
self.assertEqual(
{
path.relative_to(MAIN_LOOP_ROOT).as_posix()
for path in MAIN_LOOP_ROOT.rglob("*")
if path.is_file() and "__pycache__" not in path.parts
},
{
"SKILL.md",
"rules/session-boundary.md",
"workflows/single-session.md",
"workflows/feature-planning.md",
"workflows/ticket-execution.md",
"workflows/feature-integration.md",
"scripts/main_loop.py",
"scripts/main_loop_scheduler.py",
},
)
self.assertFalse((ROOT / "scripts/main_loop.py").exists())
self.assertFalse((ROOT / "scripts/main_loop_scheduler.py").exists())
for required in (
"`TicketId`qualified `feature-slug/NN`",
"`FeatureIntegrationId``feature-slug@integrated`",
"**Blocked by:** None",
"**Blocked by:** feature-a/01; feature-b@integrated",
"同批重复 `--feature`",
"hard cut",
):
self.assertIn(required, planning)
self.assertIn("第三方 `to-tickets` 只定义通用 tracker 行为", planning)
self.assertIn("最终机器校验边界", planning)
self.assertIn("手工修改 ticket `Status`", bundle)
self.assertIn("<COOK_IT_THROUGH_ROOT>/scripts/main_loop.py", bundle)
self.assertNotIn("<PLAYBOOK_SCRIPTS>", bundle)
self.assertIn("`cook-it-through`", rules)
self.assertNotIn("**Blocked by:**", rules)
self.assertIn("只在第一方 `skills/cook-it-through/` 定义", readme)
self.assertNotIn("main-loop-ticket-contract.md", readme)
self.assertNotIn("**Blocked by:** feature-a/01; feature-b@integrated", readme)
for public_readme in (readme, skills_readme):
self.assertNotIn("main_loop.py", public_readme)
self.assertNotIn("--isolation", readme)
def test_cook_it_through_uses_routed_progressive_disclosure(self):
skill = main_loop_skill_text()
bundle = main_loop_bundle_text()
description = next(
line for line in skill.splitlines() if line.startswith("description:")
)
self.assertLessEqual(len(skill.splitlines()), 85)
self.assertLessEqual(len(skill.encode("utf-8")), 8_000)
self.assertLessEqual(sum(len(p.read_text().splitlines()) for p in main_loop_instruction_paths()), 330)
self.assertIn("main_loop.py <command> --help", skill)
self.assertNotIn("```bash", bundle)
self.assertNotIn("入口 1", description)
for command in subcommand_parsers():
self.assertNotIn(command, description)
for negative_boundary in ("纯 TSL 语法/API 查询", "commit message", "远端 Gitea CI"):
self.assertIn(negative_boundary, description)
for path in main_loop_instruction_paths()[1:]:
relative = path.relative_to(MAIN_LOOP_ROOT).as_posix()
other_text = "\n".join(
candidate.read_text(encoding="utf-8")
for candidate in main_loop_instruction_paths()
if candidate != path
)
self.assertIn(relative, other_text, msg=f"unrouted instruction file: {relative}")
self.assertNotIn("FILL:", bundle)
def test_cook_it_through_keeps_irrecoverable_red_lines_resident(self):
skill = normalized_prose(main_loop_skill_text())
rules = rules_text()
for required in (
"禁止手工修改 ticket `Status`",
"禁止伪造或复用证据 artifact",
"integration dependency 不可见时禁止继续",
"禁止 stash、reset 或覆盖其他 session 改动",
"远程 tracker、独立 clone 或跨机器状态",
):
self.assertIn(required, skill)
for migrated_rule in (
"main-loop:ticket-state",
"integration frontier",
"远程 tracker、独立 clone、跨机器状态",
):
self.assertNotIn(migrated_rule, rules)
def test_gitignore_template_tracks_scratch_and_ignores_only_runtime(self):
template = (TEMPLATES / "gitignore.template").read_text(encoding="utf-8")
for durable_rule in ("!/.scratch/", "!/.scratch/**"):
self.assertIn(durable_rule, template)
for runtime_rule in (
"/.scratch/*.lock",
"/.scratch/worktrees/",
"/.scratch/**/*.tmp",
):
self.assertIn(runtime_rule, template)
self.assertNotIn("是否纳入版本控制由项目决定", rules_text())
def test_cook_it_through_routes_entries_and_blast_radius_floor(self):
skill = main_loop_skill_text()
entries = section(skill, "## 任务路由", "## 常驻红线")
entry_headings = [h for h in headings(entries) if h.startswith("入口 ")]
self.assertEqual(
entry_headings,
[
"入口 1:直接执行",
"入口 2:单切片改动",
"入口 3:已明确预期行为的 bug",
"入口 4:新 feature 或设计变更",
],
)
self.assertEqual(entries.count("**升级条件**"), 2)
self.assertIn("AGENT_RULES.local.md", entries)
self.assertIn("高爆炸半径路径", entries)
self.assertIn("构建、CI 或分发配置", entries)
self.assertIn("最低入口 2", entries)
self.assertIn("入口 1 不加载按需文件", entries)
entry_two = normalized_prose(section(entries, "### 入口 2", "### 入口 3"))
entry_four = normalized_prose(entries.split("### 入口 4", 1)[1])
self.assertIn("不属于入口 3", entry_two)
self.assertIn("入口 4", entry_two)
self.assertIn("边界不清时先按入口 2 起步", entry_four)
def test_cook_it_through_routes_single_session_work(self):
workflow = main_loop_workflow_text("single-session")
for required in (
"当前 `HEAD` 为 review fixed point",
"`<fixed-point>` 作为 `code-review` 的 fixed point",
"仅运行 Standards axis",
"`diagnosing-bugs` 完整执行 Phase 1-6",
"improve-codebase-architecture",
):
self.assertIn(required, workflow)
self.assertNotIn("`<fixed-point>...HEAD`", workflow)
bug = section(workflow, "## 入口 3", "## 完成与升级")
self.assertNotIn("grill-with-docs", bug)
def test_cook_it_through_routes_feature_planning_and_onramps(self):
planning = main_loop_workflow_text("feature-planning")
for required in (
"`wayfinder`",
"`to-spec -> to-tickets`",
"`research`",
"先进入 `grill-with-docs`",
"首次运行 `setup-matt-pocock-skills`",
"seam confirmation 在 `to-spec` 与 `tdd`",
"`tdd` 不得在未经确认的 seam 上开始",
):
self.assertIn(required, planning)
self.assertNotIn("`prototype`", planning)
def test_cook_it_through_commits_planning_baseline_before_claim(self):
planning = main_loop_workflow_text("feature-planning")
ordered_steps = (
"-> to-spec",
"-> to-tickets",
"-> main_loop.py enqueue",
"-> 提交 planning baseline",
"-> main_loop.py claim",
)
positions = [planning.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
for durable_input in (
"`.scratch/<feature>/spec.md`",
"`.scratch/<feature>/issues/*.md`",
"`.scratch/queue.md`",
):
self.assertIn(durable_input, planning)
self.assertIn("任何 claim 前", planning)
self.assertIn("不隐式提交", planning)
def test_cook_it_through_defines_unattended_fallback(self):
planning = main_loop_workflow_text("feature-planning")
execution = main_loop_workflow_text("ticket-execution")
self.assertIn("尚未 claim ticket 时", planning)
self.assertIn("to-questionnaire", planning)
self.assertIn(".scratch/questions/<slug>.md", planning)
self.assertIn("从 `grill-with-docs` 恢复", normalized_prose(planning))
self.assertIn("finish --result blocked", execution)
def test_cook_it_through_binds_state_and_evidence_to_claim(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
for key in (
"FEATURE",
"TICKET",
"CONTROL_ROOT",
"STATE_ROOT",
"WORKSPACE",
"BRANCH",
"BASE",
"ISOLATION",
):
self.assertIn(f"`{key}`", execution)
self.assertIn('--state-root "<PROJECT_ROOT>/.scratch"', execution)
self.assertIn('--repo-root "<PROJECT_ROOT>"', execution)
self.assertNotIn("--repo-root .", execution)
self.assertIn("把该 `FEATURE_HEAD` 合入 ticket branch", execution)
def test_cook_it_through_defines_ticket_execution_adapter(self):
execution = main_loop_workflow_text("ticket-execution")
ordered_steps = (
"读取已领取 ticket 的 spec",
"按 `tdd`",
"提交全部实现",
"运行 `code-review` 的 Standards/Spec",
"结构化证据调用 main_loop.py finish",
)
positions = [execution.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
for required in (
"fixed point",
"`<STATE_ROOT>/<feature>/spec.md`",
"`<STATE_ROOT>/<feature>/issues/<NN>-*.md`",
"零个未解决的硬 finding",
"不给 pass/fail 判定",
"不得填写 pass",
):
self.assertIn(required, execution)
def test_cook_it_through_defines_lease_and_stuck_ticket_recovery(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
for required in (
"固定为 30 分钟",
"每 10 分钟",
"`reclaim` 只接管 stale 的 `claimed`",
"release-ticket",
"claim 环境准备失败",
"会占住该 ticket",
"原 `BASE`",
"blocked/skipped 必须给 reason",
):
self.assertIn(required, execution)
def test_cook_it_through_stops_on_integration_visibility_retry(self):
execution = main_loop_workflow_text("ticket-execution")
for key in (
"TICKET",
"DEPENDENCY",
"INTEGRATION_COMMIT",
"WORKSPACE",
"BRANCH",
"BRANCH_HEAD",
"TICKET_BRANCH",
"TICKET_BRANCH_HEAD",
"SYNC_BRANCH",
"MAIN_BRANCH",
"MAIN_HEAD",
"SYNC_COMMAND",
):
self.assertIn(f"`{key}`", execution)
self.assertIn("任一字段缺失", execution)
self.assertIn("取得正式 assignment 前不得继续", execution)
def test_cook_it_through_requires_fresh_evidence_artifacts(self):
bundle = main_loop_bundle_text()
execution = main_loop_workflow_text("ticket-execution")
integration = main_loop_workflow_text("feature-integration")
for required in (
"fresh UTF-8 JSON artifact",
"finish --help",
".scratch/<feature>/evidence/",
"无法证明命令真的执行过",
):
self.assertIn(required, execution)
for required in ("三个独立门禁,不能互相替代", "integrate --help"):
self.assertIn(required, integration)
self.assertIn("禁止伪造或复用证据 artifact", bundle)
for required in ("output_sha256", "report_sha256"):
self.assertIn(required, MAIN_LOOP.EVIDENCE_HELP)
self.assertIn("--main-verified", required_flags(subcommand_parsers()["integrate"]))
def test_cook_it_through_commits_final_state_after_integration(self):
integration = main_loop_workflow_text("feature-integration")
self.assertLess(
integration.index("main_loop.py integrate"),
integration.index("提交 final workflow state"),
)
for durable_path in (
"`.scratch/<feature>/`",
"`.scratch/queue.md`",
"`.main-loop.json`",
):
self.assertIn(durable_path, integration)
self.assertIn("不要运行 `git add .scratch`", integration)
self.assertIn("不得 amend/squash", integration)
self.assertIn("`MAIN_INTEGRATION_COMMIT`", integration)
def test_cook_it_through_orders_phase_boundary_options_and_reload(self):
boundary = normalized_prose(main_loop_session_boundary_text())
ordered_options = (
"继续当前 session",
"使用 `clear`",
"使用 `handoff`",
"交给 subagent",
"使用 `compact`",
)
positions = [boundary.index(option) for option in ordered_options]
self.assertEqual(positions, sorted(positions))
for required in (
"下一阶段需要当前 session 作为 primary source",
"约 150k tokens",
"`handoff` 解决的是可移植性",
"重新加载 `SKILL.md` 与当前路由文件",
"不得只依据 `status` 输出继续",
"`domain-modeling`",
"`codebase-design` 只作词汇来源",
):
self.assertIn(required, boundary)
def test_cook_it_through_delegates_command_semantics_to_help(self):
skill = main_loop_skill_text()
bundle = main_loop_bundle_text()
parsers = subcommand_parsers()
self.assertIn("main_loop.py <command> --help", skill)
self.assertIsNone(
re.search(r"\|\s*`?main_loop\.py (?:enqueue|status|claim|finish)", bundle),
msg="command responsibility tables duplicate argparse help",
)
for command, parser in parsers.items():
self.assertIn(f"main_loop.py {command}", bundle)
self.assertTrue(parser.description, msg=f"thin help for {command}")
for flag in required_flags(parser):
action = option_action(parser, flag)
self.assertNotIn(action.help, (None, argparse.SUPPRESS))
documented = set(re.findall(r"main_loop\.py ([a-z][a-z-]*)", bundle))
self.assertEqual(documented - set(parsers), set())
def test_cook_it_through_documents_executable_state_transitions(self):
execution = normalized_prose(main_loop_workflow_text("ticket-execution"))
finish = subcommand_parsers()["finish"]
result = option_action(finish, "--result")
self.assertEqual(
set(result.choices or ()),
{"resolved", "blocked", "released", "skipped"},
)
self.assertIn("blocked/skipped 必须给 reason", execution)
release_ticket = subcommand_parsers()["release-ticket"]
release_options = {
option
for action in release_ticket._actions # noqa: SLF001
for option in action.option_strings
}
self.assertNotIn("--repo-root", release_options)
self.assertNotIn("--owner", release_options)
def test_workflow_instructions_only_reference_installed_skills(self):
instructions = "\n".join((rules_text(), main_loop_bundle_text()))
skills = installed_skills()
machine_vocabulary = (
set(subcommand_parsers())
| set(MAIN_LOOP.ALLOWED_STATUSES)
| isolation_choices()
| NON_SKILL_BACKTICK_TOKENS
)
referenced = {
token
for token in re.findall(r"`([a-z][a-z0-9-]+)`", instructions)
if token not in machine_vocabulary
}
self.assertTrue(referenced)
self.assertEqual(sorted(referenced - skills), [])
def test_cook_it_through_limits_state_to_shared_local_markdown(self):
skill = normalized_prose(main_loop_skill_text())
self.assertIn("local Markdown tracker", skill)
self.assertIn("远程 tracker、独立 clone 或跨机器状态", skill)
self.assertNotIn("必须改用具备远程", skill)
if __name__ == "__main__":
unittest.main()