import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] TEMPLATES = ROOT / "templates" 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")) ) 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.assertIn(".scratch//spec.md", templates_readme) 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 ( "", "", "", "", ): self.assertIn(marker, lines, msg=f"missing marker: {marker}") framework_start = lines.index("") framework_end = lines.index("") agents_start = lines.index("") agents_end = lines.index("") 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 = templates_readme.split("## 模板分类", 1)[1].split( "## 模板说明", 1 )[0] self.assertIn("### 1. 入口导航", classification) self.assertIn("### 2. 初始化后由项目维护", classification) self.assertIn("### 3. 参考模板", classification) self.assertIn("项目新增的 `memory-bank/*`", classification) self.assertNotIn("[sync_", classification) self.assertNotIn("force", classification) self.assertNotIn("no_backup", classification) 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", }, ) rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") stable_paths = ( "memory-bank/project-brief.md", "memory-bank/tech-context.md", "memory-bank/system-patterns.md", ) for path in stable_paths: self.assertIn(path, rules) self.assertIn("进入 `grill-with-docs` 或本地 ticket 执行协议前", rules) normalized_rules = " ".join(rules.split()) for required in ( "只记录下一 session 仍需要的稳定知识", "写入 `tech-context.md` 的命令和环境事实必须已经验证", "关键取舍及理由写入 `docs/adr/`", "不把聊天流水、未验证猜测或短期进度写入 `CONTEXT.md`", "没有长期价值的信息时不更新这些文件", ): self.assertIn(required, normalized_rules) def test_prompt_templates_are_not_part_of_the_workflow(self): self.assertFalse(TEMPLATES.joinpath("prompts").exists()) def test_agent_rules_template_defines_ticket_and_integration_contracts(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") self.assertIn("Matt Pocock 工程流程与 ticket 执行约束", rules) self.assertIn("{{PLAYBOOK_ROOT}}", rules) self.assertIn("Playbook 模板/供应商目录", rules) self.assertIn("setup-matt-pocock-skills", rules) self.assertIn("grill-with-docs", rules) self.assertIn("to-spec", rules) self.assertIn("to-tickets", rules) self.assertIn("一次可以先生成多个 feature", rules) self.assertIn("`.scratch/queue.md`", rules) self.assertIn("多个 frontier tickets 可在 worktree 模式并发执行", rules) self.assertIn("不创建额外 worktree", rules) self.assertIn("共享同一文件系统", rules) self.assertIn("跨机器或独立 clone", " ".join(rules.split())) self.assertIn("只有 `reclaim` 可以接管", rules) self.assertIn("先提交实现", rules) self.assertIn("Standards/Spec 双轴审查", rules) self.assertIn("三个独立门禁,不能互相替代", rules) for legacy in LEGACY_FLOW_TERMS: self.assertNotIn(legacy, rules) def test_agent_rules_routes_each_matt_on_ramp_to_its_real_destination(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") on_ramps = rules.split("## On-ramps 与 detours", 1)[1].split( "## Phase boundaries", 1 )[0] self.assertIn("`wayfinder`", on_ramps) self.assertIn("`to-spec -> to-tickets`", on_ramps) self.assertIn("`research`", on_ramps) self.assertIn("先进入 `grill-with-docs`", on_ramps) self.assertIn("`handoff` 到独立目录运行", on_ramps) self.assertIn("`prototype`", on_ramps) self.assertIn("再 `handoff` 结论回原设计会话", on_ramps) bug_route = rules.split("### 已明确预期行为的 bug", 1)[1].split( "## 正式工程主链", 1 )[0] self.assertIn("`diagnosing-bugs` 完整执行 Phase 1-6", bug_route) self.assertIn("不再进入 `implement` 或重复 `tdd`", bug_route) self.assertNotIn("to-spec", bug_route) def test_agent_rules_defines_a_local_ticket_execution_adapter(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") main_flow = rules.split("## 正式工程主链", 1)[1].split( "## On-ramps 与 detours", 1 )[0] adapter = rules.split("## 本地 Ticket 执行协议", 1)[1].split( "## 文档职责", 1 )[0] normalized_adapter = " ".join(adapter.split()) self.assertIn("本地 ticket 执行协议", main_flow) self.assertNotIn("implement + tdd", main_flow) self.assertIn("不直接调用上游 `implement`", normalized_adapter) ordered_steps = ( "读取已领取 ticket 的 spec", "按 `tdd`", "提交全部实现", "运行 `code-review`", "调用 `main_loop.py finish`", ) positions = [adapter.index(step) for step in ordered_steps] self.assertEqual(positions, sorted(positions)) def test_agent_rules_defines_mechanical_review_inputs_and_pass_mapping(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") review_contract = rules.split("### Review 适配契约", 1)[1].split( "### Ticket 完成或状态转换", 1 )[0] normalized_review_contract = " ".join(review_contract.split()) for required in ( "fixed point", "`.scratch//spec.md`", "`.scratch//issues/-*.md`", "零个未解决的硬 finding", "不得据此填写 `standards=pass` 或 `spec=pass`", ): self.assertIn(required, normalized_review_contract) def test_agent_rules_reads_claimed_context_after_claim_and_defines_lease_policy(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") startup = rules.split("## 会话启动", 1)[1].split("## 任务入口", 1)[0] claim = rules.split("### 领取", 1)[1].split("### 心跳和接管", 1)[0] lease = rules.split("### 心跳和接管", 1)[1].split( "### Review 适配契约", 1 )[0] self.assertNotIn("当前 `.scratch//spec.md`", startup) self.assertNotIn("当前 `.scratch//issues/.md`", startup) self.assertIn("领取成功后立即读取", claim) self.assertIn("全局唯一", claim) self.assertIn("每 10 分钟", lease) self.assertIn("固定为 30 分钟", lease) def test_agent_rules_preserves_to_spec_seam_and_phase_boundary_contracts(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") main_flow = rules.split("## 正式工程主链", 1)[1].split( "## On-ramps 与 detours", 1 )[0] self.assertIn("不重新进行已经完成的需求采访", main_flow) self.assertIn("完成 seam confirmation", main_flow) self.assertIn("`tdd` 不得在未经确认的 seam 上开始", main_flow) phase_boundaries = rules.split("## Phase boundaries", 1)[1].split( "## 文档职责", 1 )[0] ordered_options = ( "继续当前 session", "使用 `clear`", "使用 `handoff`", "交给 subagent", "使用 `compact`", ) positions = [phase_boundaries.index(option) for option in ordered_options] self.assertEqual(positions, sorted(positions)) self.assertIn("上下文过长不等于必须 `handoff`", phase_boundaries) def test_agent_rules_binds_state_and_evidence_to_claimed_git_context(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") execution = rules.split("### 领取", 1)[1] after_claim_output = execution.split("stdout 返回", 1)[1] self.assertIn("绝对 `STATE_ROOT`", execution) self.assertIn("`CONTROL_ROOT` 是共享 Git control checkout", execution) self.assertIn("`WORKSPACE` 是当前 ticket 的代码工作区", execution) self.assertNotIn("--state-root .scratch", after_claim_output) self.assertIn("--review-base <同一feature HEAD>", execution) self.assertIn( 'commit=; base=; standards=pass; spec=pass', execution, ) self.assertIn("把该 `FEATURE_HEAD` 合入 ticket branch", execution) self.assertIn( "`--feature-head`、`--review-base` 和 review evidence 的 `base`", execution, ) self.assertIn( "base=<最新main HEAD>; standards=pass; spec=pass", execution, ) def test_agent_rules_limits_main_loop_to_shared_local_markdown_state(self): rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8") isolation = rules.split("## 执行隔离", 1)[1].split( "## 主循环命令", 1 )[0] self.assertIn("`main_loop.py` 只支持 local Markdown tracker", isolation) self.assertIn("没有远程 tracker adapter", isolation) self.assertIn("跨机器或独立\nclone 的并发不受支持", isolation) self.assertNotIn("必须改用具备远程", isolation) if __name__ == "__main__": unittest.main()