✨ feat(workflow): adopt Matt Pocock ticket workflow
Replace the Superpowers plan pipeline with grill-with-docs, specs, local tickets, and ticket-native execution. BREAKING CHANGE: Remove the legacy Plan CLI, prompt templates, and Superpowers skills.
This commit is contained in:
+15
-3
@@ -9,9 +9,11 @@ test/
|
||||
├── README.md # 本文件:测试文档
|
||||
├── test_playbook.py # snapshot/subtree 参数化完整部署测试
|
||||
├── test_template_contracts.py # 模板内容、占位符、文案契约测试
|
||||
├── test_main_loop_cli.py # main_loop CLI 测试
|
||||
├── test_main_loop_cli.py # ticket 调度、隔离、证据与集成测试
|
||||
├── agent/ # Agent 题面/运行时验证测试定义
|
||||
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
|
||||
├── test_tsl_playbook_sync.py # TSL Playbook 构建与同步边界测试
|
||||
├── test_tsl_syntax_reference.py # TSL 语法文档结构与检索测试
|
||||
└── integration/ # 集成测试
|
||||
└── check_doc_links.py # 文档链接有效性检查
|
||||
```
|
||||
@@ -23,7 +25,7 @@ test/
|
||||
cd /path/to/playbook
|
||||
|
||||
# 1. 运行 Python 测试(test/ 下的 test_*.py)
|
||||
python -m unittest discover -s test -p "test_*.py" -v
|
||||
python -X utf8 -B -m unittest discover -s test -p "test_*.py" -v
|
||||
|
||||
# 2. 运行文档链接检查
|
||||
python test/integration/check_doc_links.py
|
||||
@@ -53,6 +55,16 @@ python test/integration/check_doc_links.py
|
||||
|
||||
随 Python 测试检查通用模板的关键 marker、占位符和流程合同。
|
||||
|
||||
### 3. 文档链接检查 (integration/)
|
||||
### 3. 主循环测试 (`test_main_loop_cli.py`)
|
||||
|
||||
使用临时 Git 仓库验证 ticket 队列、串行与 worktree 隔离、并发 claim、heartbeat/reclaim、
|
||||
验证与 review 证据绑定,以及 ticket/feature 两级集成。
|
||||
|
||||
### 4. TSL 契约测试
|
||||
|
||||
- `test_tsl_playbook_sync.py`:验证 TSL Playbook 构建和同步文件边界
|
||||
- `test_tsl_syntax_reference.py`:验证语法文档结构、section ID 与检索结果
|
||||
|
||||
### 5. 文档链接检查 (integration/)
|
||||
|
||||
扫描 `docs/` 与模板文件中的本地链接,确保引用路径有效。
|
||||
|
||||
+2427
-882
File diff suppressed because it is too large
Load Diff
+181
-17
@@ -76,9 +76,6 @@ no_backup = true
|
||||
project_name = "Demo"
|
||||
no_backup = true
|
||||
|
||||
[sync_prompts]
|
||||
no_backup = true
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl", "markdown"]
|
||||
gitattr_mode = "overwrite"
|
||||
@@ -101,18 +98,128 @@ def seed_custom_files(project_root: Path) -> None:
|
||||
custom_memory.parent.mkdir(parents=True)
|
||||
custom_memory.write_text("custom memory\n", encoding="utf-8", newline="\n")
|
||||
|
||||
custom_prompt = project_root / "docs" / "prompts" / "custom.md"
|
||||
custom_prompt.parent.mkdir(parents=True)
|
||||
custom_prompt.write_text("custom prompt\n", encoding="utf-8", newline="\n")
|
||||
|
||||
(project_root / "AGENTS.md").write_text(
|
||||
"# Existing Agents\n\nKeep this agent note.\n\nSee `.agents/index.md`.\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
(project_root / "CLAUDE.md").write_text(
|
||||
"# Existing Claude\n\nKeep this.\n",
|
||||
"# Existing Claude\n\nKeep this Claude note.\n\n@AGENTS.md\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
|
||||
class PlaybookDeploymentTests(unittest.TestCase):
|
||||
def test_help_lists_only_current_cli_options(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--help"],
|
||||
cwd=Path(tmp_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertIn("-config PATH", result.stdout)
|
||||
self.assertIn("-h, --help", result.stdout)
|
||||
self.assertNotIn("-h, -help", result.stdout)
|
||||
|
||||
def test_install_all_excludes_legacy_superpowers_skills(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
config = project_root / "playbook.toml"
|
||||
config.write_text(
|
||||
"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "custom/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
mode = "all"
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
0,
|
||||
msg=f"all-skills install failed\n{result.stdout}{result.stderr}",
|
||||
)
|
||||
installed = {
|
||||
path.name
|
||||
for path in (project_root / ".test-agents" / "skills").iterdir()
|
||||
if path.is_dir()
|
||||
}
|
||||
self.assertIn("grill-with-docs", installed)
|
||||
self.assertIn("to-tickets", installed)
|
||||
self.assertTrue(
|
||||
{
|
||||
"using-superpowers",
|
||||
"brainstorming",
|
||||
"writing-plans",
|
||||
"executing-plans",
|
||||
}.isdisjoint(installed)
|
||||
)
|
||||
|
||||
def test_install_skills_list_requires_explicit_skills(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
config = project_root / "playbook.toml"
|
||||
config.write_text(
|
||||
"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "custom/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
mode = "list"
|
||||
bundles = ["matt-pocock-workflow"]
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
2,
|
||||
msg=f"bundles unexpectedly supported\n{result.stdout}{result.stderr}",
|
||||
)
|
||||
self.assertIn("ERROR: skills is required", result.stderr)
|
||||
|
||||
def test_invalid_toml_is_reported_without_a_traceback(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
config = project_root / "playbook.toml"
|
||||
config.write_text(
|
||||
r"""
|
||||
[playbook]
|
||||
project_root = "C:\workspace\project"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("ERROR: invalid TOML", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_playbook_deployment_modes(self):
|
||||
for install_mode, playbook_root in MODE_ROOTS.items():
|
||||
with self.subTest(install_mode=install_mode):
|
||||
@@ -149,8 +256,7 @@ class PlaybookDeploymentTests(unittest.TestCase):
|
||||
"CLAUDE.md",
|
||||
".gitattributes",
|
||||
"memory-bank/project-brief.md",
|
||||
"memory-bank/active-context.md",
|
||||
"docs/prompts/system/agent-behavior.md",
|
||||
"memory-bank/system-patterns.md",
|
||||
".agents/index.md",
|
||||
".agents/tsl/index.md",
|
||||
".agents/markdown/index.md",
|
||||
@@ -171,17 +277,18 @@ class PlaybookDeploymentTests(unittest.TestCase):
|
||||
),
|
||||
"custom memory\n",
|
||||
)
|
||||
self.assertEqual(
|
||||
(project_root / "docs/prompts/custom.md").read_text(
|
||||
encoding="utf-8"
|
||||
),
|
||||
"custom prompt\n",
|
||||
agents_text = (project_root / "AGENTS.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("Keep this agent note.", agents_text)
|
||||
self.assertEqual(
|
||||
agents_text.count("<!-- playbook:framework:start -->"), 1
|
||||
)
|
||||
|
||||
claude_text = (project_root / "CLAUDE.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("Keep this.", claude_text)
|
||||
self.assertIn("Keep this Claude note.", claude_text)
|
||||
self.assertIn("@AGENTS.md", claude_text)
|
||||
self.assertEqual(
|
||||
claude_text.count("<!-- playbook:claude:start -->"), 1
|
||||
)
|
||||
@@ -232,10 +339,67 @@ class PlaybookDeploymentTests(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
(snapshot_root / "scripts/playbook.py").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(snapshot_root / "scripts/main_loop.py").is_file()
|
||||
)
|
||||
self.assertTrue(
|
||||
(snapshot_root / "playbook.example.toml").is_file()
|
||||
)
|
||||
self.assertFalse(
|
||||
(snapshot_root / "playbook.toml.example").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
(snapshot_root / "scripts/ticket_loop.py").exists()
|
||||
)
|
||||
self.assertTrue((snapshot_root / "skills").is_dir())
|
||||
else:
|
||||
self.assertFalse((source_root / "SOURCE.md").exists())
|
||||
|
||||
def test_sync_without_standards_keeps_language_rules(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
playbook_root = MODE_ROOTS["snapshot"]
|
||||
|
||||
full_config = write_config(project_root, "snapshot", playbook_root)
|
||||
result = run_playbook(SCRIPT, full_config, project_root)
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
0,
|
||||
msg=f"seed run failed\n{result.stdout}{result.stderr}",
|
||||
)
|
||||
|
||||
agents_md = project_root / "AGENTS.md"
|
||||
seeded = agents_md.read_text(encoding="utf-8")
|
||||
self.assertIn("`.agents/tsl/index.md`", seeded)
|
||||
self.assertIn("`.agents/markdown/index.md`", seeded)
|
||||
|
||||
partial_config = project_root / "playbook-partial.toml"
|
||||
partial_config.write_text(
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "{playbook_root.as_posix()}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
result = run_playbook(SCRIPT, partial_config, project_root)
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
0,
|
||||
msg=f"partial run failed\n{result.stdout}{result.stderr}",
|
||||
)
|
||||
|
||||
after = agents_md.read_text(encoding="utf-8")
|
||||
self.assertIn("`.agents/tsl/index.md`", after)
|
||||
self.assertIn("`.agents/markdown/index.md`", after)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+267
-156
@@ -1,31 +1,69 @@
|
||||
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_project_templates_drop_legacy_language_placeholders(self):
|
||||
templates_readme = (ROOT / "templates" / "README.md").read_text(
|
||||
encoding="utf-8"
|
||||
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.assertIn("docs/superpowers/", templates_readme)
|
||||
self.assertIn("docs/prompts/README.md", templates_readme)
|
||||
self.assertIn("完整主链只在 `AGENT_RULES.template.md` 定义", templates_readme)
|
||||
self.assertNotIn("docs/superpowers/", templates_readme)
|
||||
self.assertIn(".scratch/<feature>/spec.md", templates_readme)
|
||||
self.assertNotIn("docs/prompts/", templates_readme)
|
||||
|
||||
agents_template = (ROOT / "templates" / "AGENTS.template.md").read_text(
|
||||
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("docs/prompts/README.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 = (
|
||||
ROOT / "templates" / "memory-bank" / "tech-context.template.md"
|
||||
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)
|
||||
@@ -33,166 +71,239 @@ class TemplateContractsTests(unittest.TestCase):
|
||||
self.assertIn("## 不可假设项", tech_context_template)
|
||||
|
||||
project_brief_template = (
|
||||
ROOT / "templates" / "memory-bank" / "project-brief.template.md"
|
||||
TEMPLATES / "memory-bank" / "project-brief.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("## 成功定义", project_brief_template)
|
||||
|
||||
def test_memory_bank_templates_capture_short_lived_context_contracts(self):
|
||||
progress_template = (
|
||||
ROOT / "templates" / "memory-bank" / "progress.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("短期状态快照,不是 changelog", progress_template)
|
||||
self.assertIn("`Recent Changes` 只保留最近 3-5 条", progress_template)
|
||||
self.assertIn("整理/替换旧摘要,不做无限追加", progress_template)
|
||||
|
||||
active_context_template = (
|
||||
ROOT / "templates" / "memory-bank" / "active-context.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("短期上下文快照,不是长期日志", active_context_template)
|
||||
self.assertIn("`Recent Changes` 只保留最近 3-5 条", active_context_template)
|
||||
self.assertIn(
|
||||
"`Touched Files` 只保留当前 Plan / 下一轮仍相关的文件",
|
||||
active_context_template,
|
||||
)
|
||||
self.assertIn("整理/替换旧上下文,不做无限追加", active_context_template)
|
||||
|
||||
def test_prompt_templates_point_to_current_superpowers_flow(self):
|
||||
update_memory_template = (
|
||||
ROOT / "templates" / "prompts" / "coding" / "update-memory.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertNotIn("workflow-state", update_memory_template)
|
||||
self.assertIn("plan-status", update_memory_template)
|
||||
self.assertIn("## Updated Files", update_memory_template)
|
||||
self.assertIn("## New Context", update_memory_template)
|
||||
self.assertIn("## Outstanding Risks", update_memory_template)
|
||||
self.assertIn("AGENT_RULES.local.md", update_memory_template)
|
||||
self.assertIn("短期状态快照", update_memory_template)
|
||||
self.assertIn(
|
||||
"不要把 `Recent Changes` 当作无限追加日志", update_memory_template
|
||||
)
|
||||
self.assertIn("短期上下文快照", update_memory_template)
|
||||
self.assertIn(
|
||||
"`Touched Files` 只保留当前 Plan / 下一轮仍相关的文件",
|
||||
update_memory_template,
|
||||
)
|
||||
|
||||
close_task_template = (
|
||||
ROOT / "templates" / "prompts" / "coding" / "close-task.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("main_loop.py finish", close_task_template)
|
||||
self.assertIn("如本轮来自 `main_loop.py claim`", close_task_template)
|
||||
self.assertNotIn("workflow-state", close_task_template)
|
||||
self.assertIn("未归档不得声明 Plan 完成", close_task_template)
|
||||
self.assertIn("按项目归档机制只归档", close_task_template)
|
||||
self.assertIn("状态已写回,交付未完成", close_task_template)
|
||||
self.assertIn("## Completed", close_task_template)
|
||||
self.assertIn("## Not Completed", close_task_template)
|
||||
self.assertIn("## Verification", close_task_template)
|
||||
self.assertIn("## Risks", close_task_template)
|
||||
self.assertIn("## Next Steps", close_task_template)
|
||||
|
||||
verify_change_template = (
|
||||
ROOT / "templates" / "prompts" / "coding" / "verify-change.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("如本轮来自 `main_loop.py claim`", verify_change_template)
|
||||
self.assertNotIn("workflow-state", verify_change_template)
|
||||
self.assertIn("plan-status", verify_change_template)
|
||||
self.assertIn("验证通过不等于 Plan 完成", verify_change_template)
|
||||
self.assertIn(
|
||||
"当前 Plan 相关差异未归档/提交时,不得声明 Plan 完成",
|
||||
verify_change_template,
|
||||
)
|
||||
self.assertIn("## Validated", verify_change_template)
|
||||
self.assertIn("## Evidence", verify_change_template)
|
||||
self.assertIn("## Not Validated", verify_change_template)
|
||||
self.assertIn("## Risks", verify_change_template)
|
||||
|
||||
clarify_template = (
|
||||
ROOT / "templates" / "prompts" / "coding" / "clarify.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertNotIn("Vibe-coding", clarify_template)
|
||||
self.assertIn("## Current Understanding", clarify_template)
|
||||
self.assertIn("## Open Question", clarify_template)
|
||||
self.assertIn("## Recommended Default", clarify_template)
|
||||
|
||||
code_review_template = (
|
||||
ROOT / "templates" / "prompts" / "coding" / "code-review.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("## Findings", code_review_template)
|
||||
self.assertIn("## Open Questions", code_review_template)
|
||||
self.assertIn("## Residual Risk", code_review_template)
|
||||
|
||||
prompts_readme = (ROOT / "templates" / "prompts" / "README.md").read_text(
|
||||
def test_agents_template_nests_agents_block_inside_framework_block(self):
|
||||
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("AGENT_RULES.md", prompts_readme)
|
||||
self.assertIn("docs/superpowers/", prompts_readme)
|
||||
self.assertIn("不是流程权威", prompts_readme)
|
||||
self.assertNotIn("playbook.py -record-spec", prompts_readme)
|
||||
self.assertNotIn("playbook.py -record-plan", prompts_readme)
|
||||
self.assertNotIn("using-superpowers", prompts_readme)
|
||||
self.assertNotIn("main_loop.py claim", prompts_readme)
|
||||
self.assertNotIn("docs/workflows/", prompts_readme)
|
||||
self.assertIn("设计与计划产物", prompts_readme)
|
||||
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}")
|
||||
|
||||
agent_behavior_template = (
|
||||
ROOT / "templates" / "prompts" / "system" / "agent-behavior.template.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("AGENT_RULES.md", agent_behavior_template)
|
||||
self.assertIn("AGENT_RULES.local.md", agent_behavior_template)
|
||||
self.assertIn("docs/superpowers/specs/", agent_behavior_template)
|
||||
self.assertIn("docs/superpowers/plans/", agent_behavior_template)
|
||||
self.assertNotIn("using-superpowers", 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-plan", agent_behavior_template)
|
||||
self.assertIn(
|
||||
"项目上下文与执行状态写入 `memory-bank/`", agent_behavior_template
|
||||
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 = 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",
|
||||
},
|
||||
)
|
||||
|
||||
def test_agent_rules_template_defines_plan_and_archival_contracts(self):
|
||||
rules_template = (ROOT / "templates" / "AGENT_RULES.template.md").read_text(
|
||||
encoding="utf-8"
|
||||
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",
|
||||
)
|
||||
self.assertIn("框架流程约束基线", rules_template)
|
||||
self.assertIn("{{PLAYBOOK_ROOT}}", rules_template)
|
||||
self.assertIn("Playbook 模板/供应商目录", rules_template)
|
||||
self.assertIn("唯一设计与计划产物中心", rules_template)
|
||||
self.assertIn("memory-bank/progress.md", rules_template)
|
||||
self.assertIn("优先恢复 `in-progress`", rules_template)
|
||||
self.assertIn("选择第一个可执行 Plan", rules_template)
|
||||
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/<feature>/spec.md`",
|
||||
"`.scratch/<feature>/issues/<ticket>-*.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/<feature>/spec.md`", startup)
|
||||
self.assertNotIn("当前 `.scratch/<feature>/issues/<ticket>.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(
|
||||
"`main_loop.py finish -status done` 只负责写回机器状态",
|
||||
rules_template,
|
||||
'commit=<HEAD>; base=<review-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(
|
||||
"Plan `done` 后必须完成当前 Plan 变更的归档/提交", rules_template
|
||||
"base=<最新main HEAD>; standards=pass; spec=pass",
|
||||
execution,
|
||||
)
|
||||
self.assertIn(
|
||||
"spec/plan 产出阶段不单独提交或归档", rules_template
|
||||
)
|
||||
self.assertIn(
|
||||
"如外部 skill", rules_template
|
||||
)
|
||||
self.assertIn("Plan 范围是归档/提交边界", rules_template)
|
||||
self.assertIn(
|
||||
"不自动提交或归档",
|
||||
rules_template,
|
||||
)
|
||||
self.assertIn("当前 Plan 文件", rules_template)
|
||||
self.assertIn("`memory-bank/progress.md`", rules_template)
|
||||
self.assertIn("必要 memory 更新", rules_template)
|
||||
self.assertIn("允许其未归档共存", rules_template)
|
||||
self.assertIn("当前 Plan 无遗留差异", rules_template)
|
||||
self.assertIn("状态已写回,交付未完成", rules_template)
|
||||
self.assertNotIn("如项目使用 Git", rules_template)
|
||||
self.assertNotIn("Git 提交", rules_template)
|
||||
self.assertNotIn("git status", rules_template)
|
||||
self.assertNotIn("git commit", rules_template)
|
||||
self.assertNotIn("git worktree", rules_template)
|
||||
self.assertNotIn(".gitattributes", rules_template)
|
||||
self.assertIn("`progress.md` 上半部分是短期状态快照", rules_template)
|
||||
self.assertIn("`active-context.md` 是短期上下文快照", rules_template)
|
||||
|
||||
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__":
|
||||
|
||||
@@ -16,7 +16,9 @@ LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers
|
||||
UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh"
|
||||
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
|
||||
SKILLS_MD = ROOT / "SKILLS.md"
|
||||
SUPERPOWERS_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "superpowers.list"
|
||||
MATT_POCOCK_LIST = (
|
||||
ROOT / "skills" / "thirdparty" / ".sources" / "matt-pocock-skills.list"
|
||||
)
|
||||
UI_UX_PRO_MAX_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "ui-ux-pro-max.list"
|
||||
UI_UX_PRO_MAX_DIR = ROOT / "skills" / "thirdparty" / "ui-ux-pro-max"
|
||||
BROOKS_LINT_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "brooks-lint.list"
|
||||
@@ -62,7 +64,7 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
[entry["id"] for entry in data["sources"]],
|
||||
[
|
||||
"superpowers",
|
||||
"matt-pocock-skills",
|
||||
"ui-ux-pro-max",
|
||||
"andrej-karpathy-skills",
|
||||
"brooks-lint",
|
||||
@@ -72,6 +74,62 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_matt_pocock_manifest_syncs_stable_skill_groups(self):
|
||||
data = load_manifest()
|
||||
matt = next(
|
||||
item for item in data["sources"] if item["id"] == "matt-pocock-skills"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
matt["upstream_repo"], "https://github.com/mattpocock/skills.git"
|
||||
)
|
||||
self.assertEqual(matt["snapshot_dir"], "matt-pocock-skills")
|
||||
self.assertEqual(matt["sync_mode"], "copy_skill_dirs")
|
||||
self.assertEqual(
|
||||
matt["skills_subdirs"],
|
||||
["skills/engineering", "skills/productivity", "skills/misc"],
|
||||
)
|
||||
self.assertIn("grill-with-docs", matt["include_skill_dirs"])
|
||||
self.assertIn("grilling", matt["include_skill_dirs"])
|
||||
self.assertIn("to-tickets", matt["include_skill_dirs"])
|
||||
|
||||
def test_matt_pocock_source_and_required_workflow_skills_are_materialized(self):
|
||||
self.assertTrue(MATT_POCOCK_LIST.is_file())
|
||||
|
||||
synced = set(MATT_POCOCK_LIST.read_text(encoding="utf-8").splitlines())
|
||||
manifest_skills = set(
|
||||
next(
|
||||
item
|
||||
for item in load_manifest()["sources"]
|
||||
if item["id"] == "matt-pocock-skills"
|
||||
)["include_skill_dirs"]
|
||||
)
|
||||
required = {
|
||||
"setup-matt-pocock-skills",
|
||||
"grill-with-docs",
|
||||
"grilling",
|
||||
"domain-modeling",
|
||||
"to-spec",
|
||||
"to-tickets",
|
||||
"implement",
|
||||
"tdd",
|
||||
"codebase-design",
|
||||
"code-review",
|
||||
"handoff",
|
||||
}
|
||||
|
||||
self.assertEqual(synced, manifest_skills)
|
||||
self.assertTrue(required <= synced)
|
||||
legacy_main_chain = {
|
||||
"using-superpowers",
|
||||
"brainstorming",
|
||||
"writing-plans",
|
||||
"executing-plans",
|
||||
}
|
||||
self.assertTrue(legacy_main_chain.isdisjoint(synced))
|
||||
for name in required:
|
||||
self.assertTrue((ROOT / "skills" / "thirdparty" / name / "SKILL.md").is_file())
|
||||
|
||||
def test_karpathy_manifest_uses_copy_skill_dirs_sync_mode(self):
|
||||
data = load_manifest()
|
||||
karpathy = next(
|
||||
@@ -144,11 +202,6 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(craft["include_skill_dirs"], ["uncle-bob-craft"])
|
||||
|
||||
def test_superpowers_manifest_prunes_non_superpowers_paths(self):
|
||||
data = load_manifest()
|
||||
superpowers = next(item for item in data["sources"] if item["id"] == "superpowers")
|
||||
self.assertEqual(superpowers["remove_paths"], ["skills/ui-ux-pro-max"])
|
||||
|
||||
def test_workflow_inlines_update_and_sync_in_single_serial_job(self):
|
||||
text = WORKFLOW.read_text(encoding="utf-8")
|
||||
self.assertFalse(LEGACY_WORKFLOW.exists())
|
||||
@@ -201,14 +254,41 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
self.assertIn("skills/thirdparty/", text)
|
||||
self.assertNotIn("Third-party Skills (superpowers)", text)
|
||||
|
||||
def test_superpowers_and_ui_ux_pro_max_source_lists_exist(self):
|
||||
self.assertTrue(SUPERPOWERS_LIST.is_file())
|
||||
def test_non_legacy_thirdparty_source_lists_exist(self):
|
||||
self.assertTrue(UI_UX_PRO_MAX_LIST.is_file())
|
||||
self.assertTrue(CODEBASE_RECON_LIST.is_file())
|
||||
self.assertIn("using-superpowers", SUPERPOWERS_LIST.read_text(encoding="utf-8"))
|
||||
self.assertIn("ui-ux-pro-max", UI_UX_PRO_MAX_LIST.read_text(encoding="utf-8"))
|
||||
self.assertIn("pathfinding", CODEBASE_RECON_LIST.read_text(encoding="utf-8"))
|
||||
|
||||
def test_superpowers_source_and_vendored_skills_are_absent(self):
|
||||
data = load_manifest()
|
||||
self.assertNotIn("superpowers", {item["id"] for item in data["sources"]})
|
||||
source_list = (
|
||||
ROOT / "skills" / "thirdparty" / ".sources" / "superpowers.list"
|
||||
)
|
||||
self.assertFalse(source_list.exists())
|
||||
legacy_skill_dirs = {
|
||||
"brainstorming",
|
||||
"dispatching-parallel-agents",
|
||||
"executing-plans",
|
||||
"finishing-a-development-branch",
|
||||
"receiving-code-review",
|
||||
"requesting-code-review",
|
||||
"subagent-driven-development",
|
||||
"systematic-debugging",
|
||||
"test-driven-development",
|
||||
"using-git-worktrees",
|
||||
"using-superpowers",
|
||||
"verification-before-completion",
|
||||
"writing-plans",
|
||||
"writing-skills",
|
||||
}
|
||||
thirdparty_root = ROOT / "skills" / "thirdparty"
|
||||
self.assertEqual(
|
||||
{name for name in legacy_skill_dirs if (thirdparty_root / name).exists()},
|
||||
set(),
|
||||
)
|
||||
|
||||
def test_codebase_recon_pathfinding_dependency_is_synced(self):
|
||||
self.assertTrue((PATHFINDING_DIR / "SKILL.md").is_file())
|
||||
self.assertTrue(
|
||||
@@ -251,7 +331,15 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
|
||||
mirror = tmp_root / "origin.git"
|
||||
work = tmp_root / "work"
|
||||
|
||||
clone_mirror = run_command("git", "clone", "--mirror", str(ROOT), str(mirror))
|
||||
clone_mirror = run_command(
|
||||
"git",
|
||||
"-c",
|
||||
f"safe.directory={(ROOT / '.git').as_posix()}",
|
||||
"clone",
|
||||
"--mirror",
|
||||
str(ROOT),
|
||||
str(mirror),
|
||||
)
|
||||
self.assertEqual(clone_mirror.returncode, 0, msg=clone_mirror.stderr)
|
||||
|
||||
main_ref = run_command(
|
||||
|
||||
Reference in New Issue
Block a user