♻️ refactor(cook-it-through): move workflow engine into skill
This commit is contained in:
+228
-11
@@ -52,7 +52,7 @@ def copy_subtree_source(destination: Path) -> None:
|
||||
)
|
||||
|
||||
(destination / "skills").mkdir()
|
||||
for name in ("commit-message",):
|
||||
for name in ("commit-message", "cook-it-through"):
|
||||
shutil.copytree(
|
||||
ROOT / "skills" / name,
|
||||
destination / "skills" / name,
|
||||
@@ -84,7 +84,7 @@ no_backup = true
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
mode = "list"
|
||||
skills = ["commit-message"]
|
||||
skills = ["commit-message", "cook-it-through"]
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
@@ -130,7 +130,7 @@ class PlaybookDeploymentTests(unittest.TestCase):
|
||||
self.assertIn("-h, --help", result.stdout)
|
||||
self.assertNotIn("-h, -help", result.stdout)
|
||||
|
||||
def test_install_all_excludes_legacy_superpowers_skills(self):
|
||||
def test_install_all_honors_configured_and_legacy_exclusions(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
@@ -145,6 +145,7 @@ install_mode = "snapshot"
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
mode = "all"
|
||||
exclude = ["cook-it-through", "to-tickets"]
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
@@ -164,7 +165,8 @@ no_backup = true
|
||||
if path.is_dir()
|
||||
}
|
||||
self.assertIn("grill-with-docs", installed)
|
||||
self.assertIn("to-tickets", installed)
|
||||
self.assertNotIn("to-tickets", installed)
|
||||
self.assertNotIn("cook-it-through", installed)
|
||||
self.assertTrue(
|
||||
{
|
||||
"using-superpowers",
|
||||
@@ -174,6 +176,97 @@ no_backup = true
|
||||
}.isdisjoint(installed)
|
||||
)
|
||||
|
||||
def test_sync_rules_rejects_missing_workflow_skill_before_any_write(self):
|
||||
invalid_install_configs = {
|
||||
"excluded": '\n'.join(
|
||||
(
|
||||
'mode = "all"',
|
||||
'exclude = ["cook-it-through"]',
|
||||
)
|
||||
),
|
||||
"omitted-from-list": '\n'.join(
|
||||
(
|
||||
'mode = "list"',
|
||||
'skills = ["commit-message"]',
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
for case, install_config in invalid_install_configs.items():
|
||||
with self.subTest(case=case), tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
project_root.mkdir()
|
||||
config = project_root / "playbook.toml"
|
||||
config.write_text(
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "custom/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
no_backup = true
|
||||
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
{install_config}
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(result.returncode, 2, msg=result.stdout)
|
||||
self.assertIn("cook-it-through", result.stderr)
|
||||
for untouched in (
|
||||
"custom/playbook",
|
||||
"AGENTS.md",
|
||||
"AGENT_RULES.md",
|
||||
"AGENT_RULES.local.md",
|
||||
".test-agents",
|
||||
):
|
||||
self.assertFalse(
|
||||
(project_root / untouched).exists(),
|
||||
msg=f"invalid config wrote {untouched}",
|
||||
)
|
||||
|
||||
def test_sync_rules_requires_an_install_skills_action_before_any_write(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"
|
||||
|
||||
[sync_rules]
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(result.returncode, 2, msg=result.stdout)
|
||||
self.assertIn("[install_skills]", result.stderr)
|
||||
self.assertIn("cook-it-through", result.stderr)
|
||||
for untouched in (
|
||||
"custom/playbook",
|
||||
"AGENTS.md",
|
||||
"AGENT_RULES.md",
|
||||
"AGENT_RULES.local.md",
|
||||
):
|
||||
self.assertFalse(
|
||||
(project_root / untouched).exists(),
|
||||
msg=f"invalid config wrote {untouched}",
|
||||
)
|
||||
|
||||
def test_install_skills_list_requires_explicit_skills(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
@@ -205,6 +298,46 @@ no_backup = true
|
||||
)
|
||||
self.assertIn("ERROR: skills is required", result.stderr)
|
||||
|
||||
def test_install_skills_accepts_an_empty_exclusion_list(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"
|
||||
skills = ["commit-message"]
|
||||
exclude = []
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
result = run_playbook(SCRIPT, config, project_root)
|
||||
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
0,
|
||||
msg=f"empty exclusion failed\n{result.stdout}{result.stderr}",
|
||||
)
|
||||
self.assertTrue(
|
||||
(
|
||||
project_root
|
||||
/ ".test-agents"
|
||||
/ "skills"
|
||||
/ "commit-message"
|
||||
/ "SKILL.md"
|
||||
).is_file()
|
||||
)
|
||||
|
||||
def test_invalid_toml_is_reported_without_a_traceback(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
project_root = Path(tmp_dir) / "project"
|
||||
@@ -269,6 +402,14 @@ project_root = "C:\workspace\project"
|
||||
".test-agents/skills/commit-message/SKILL.md",
|
||||
".test-agents/skills/commit-message/references/commit_policy.json",
|
||||
".test-agents/skills/commit-message/scripts/validate_commit_message.py",
|
||||
".test-agents/skills/cook-it-through/SKILL.md",
|
||||
".test-agents/skills/cook-it-through/rules/session-boundary.md",
|
||||
".test-agents/skills/cook-it-through/scripts/main_loop.py",
|
||||
".test-agents/skills/cook-it-through/scripts/main_loop_scheduler.py",
|
||||
".test-agents/skills/cook-it-through/workflows/single-session.md",
|
||||
".test-agents/skills/cook-it-through/workflows/feature-planning.md",
|
||||
".test-agents/skills/cook-it-through/workflows/ticket-execution.md",
|
||||
".test-agents/skills/cook-it-through/workflows/feature-integration.md",
|
||||
)
|
||||
missing = [
|
||||
path
|
||||
@@ -331,6 +472,40 @@ project_root = "C:\workspace\project"
|
||||
).read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
source_main_loop_root = ROOT / "skills/cook-it-through"
|
||||
installed_main_loop_root = (
|
||||
project_root / ".test-agents/skills/cook-it-through"
|
||||
)
|
||||
for relative_path in (
|
||||
"SKILL.md",
|
||||
"rules/session-boundary.md",
|
||||
"scripts/main_loop.py",
|
||||
"scripts/main_loop_scheduler.py",
|
||||
"workflows/single-session.md",
|
||||
"workflows/feature-planning.md",
|
||||
"workflows/ticket-execution.md",
|
||||
"workflows/feature-integration.md",
|
||||
):
|
||||
self.assertEqual(
|
||||
(installed_main_loop_root / relative_path).read_bytes(),
|
||||
(source_main_loop_root / relative_path).read_bytes(),
|
||||
)
|
||||
|
||||
installed_help = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(installed_main_loop_root / "scripts/main_loop.py"),
|
||||
"--help",
|
||||
],
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
installed_help.returncode, 0, msg=installed_help.stderr
|
||||
)
|
||||
self.assertIn("enqueue", installed_help.stdout)
|
||||
|
||||
rules_text = (project_root / "AGENT_RULES.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -338,6 +513,28 @@ project_root = "C:\workspace\project"
|
||||
f"`{playbook_root.as_posix()}/` 是 Playbook 模板/供应商目录",
|
||||
rules_text,
|
||||
)
|
||||
self.assertIn("`cook-it-through`", rules_text)
|
||||
self.assertNotIn("**Blocked by:**", rules_text)
|
||||
installed_main_loop_text = "\n".join(
|
||||
(installed_main_loop_root / relative_path).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
for relative_path in (
|
||||
"SKILL.md",
|
||||
"rules/session-boundary.md",
|
||||
"workflows/single-session.md",
|
||||
"workflows/feature-planning.md",
|
||||
"workflows/ticket-execution.md",
|
||||
"workflows/feature-integration.md",
|
||||
)
|
||||
)
|
||||
for contract_fragment in (
|
||||
"`TicketId`:qualified `feature-slug/NN`",
|
||||
"`FeatureIntegrationId`:`feature-slug@integrated`",
|
||||
"**Blocked by:** None",
|
||||
"**Blocked by:** feature-a/01; feature-b@integrated",
|
||||
):
|
||||
self.assertIn(contract_fragment, installed_main_loop_text)
|
||||
gitignore_text = (project_root / ".gitignore").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -355,15 +552,27 @@ project_root = "C:\workspace\project"
|
||||
self.assertIn("/.scratch/worktrees/", gitignore_text)
|
||||
self.assertIn("/.scratch/**/*.tmp", gitignore_text)
|
||||
|
||||
deployed_playbook_root = project_root / playbook_root
|
||||
deployed_main_loop_root = (
|
||||
deployed_playbook_root / "skills/cook-it-through/scripts"
|
||||
)
|
||||
self.assertTrue((deployed_main_loop_root / "main_loop.py").is_file())
|
||||
self.assertTrue(
|
||||
(deployed_main_loop_root / "main_loop_scheduler.py").is_file()
|
||||
)
|
||||
self.assertFalse(
|
||||
(deployed_playbook_root / "scripts/main_loop.py").exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
(deployed_playbook_root / "scripts/main_loop_scheduler.py").exists()
|
||||
)
|
||||
|
||||
if install_mode == "snapshot":
|
||||
snapshot_root = project_root / playbook_root
|
||||
snapshot_root = deployed_playbook_root
|
||||
self.assertTrue((snapshot_root / "SOURCE.md").is_file())
|
||||
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()
|
||||
)
|
||||
@@ -495,7 +704,9 @@ no_backup = true
|
||||
|
||||
# A project appendix outside the block, and drift inside it.
|
||||
appendix = "\n## 项目补充\n\n保留这段项目自己的说明。\n"
|
||||
drifted = seeded.replace("## 任务入口", "## 任务入口(本地改过)") + appendix
|
||||
drifted = seeded.replace(
|
||||
"## 工作流入口", "## 工作流入口(本地改过)"
|
||||
) + appendix
|
||||
rules_md.write_text(drifted, encoding="utf-8", newline="\n")
|
||||
|
||||
resync = run_playbook(SCRIPT, config, project_root)
|
||||
@@ -510,11 +721,11 @@ no_backup = true
|
||||
msg="content outside the block belongs to the project",
|
||||
)
|
||||
self.assertIn(
|
||||
"## 任务入口\n",
|
||||
"## 工作流入口\n",
|
||||
after,
|
||||
msg="the process itself is playbook-owned and must be refreshed",
|
||||
)
|
||||
self.assertNotIn("## 任务入口(本地改过)", after)
|
||||
self.assertNotIn("## 工作流入口(本地改过)", after)
|
||||
self.assertEqual(after.count("<!-- playbook:rules:start -->"), 1)
|
||||
|
||||
legacy = project_root / "legacy" / "AGENT_RULES.md"
|
||||
@@ -535,6 +746,12 @@ install_mode = "snapshot"
|
||||
[sync_rules]
|
||||
date = "2026-01-01"
|
||||
no_backup = true
|
||||
|
||||
[install_skills]
|
||||
agents_home = ".test-agents"
|
||||
mode = "list"
|
||||
skills = ["cook-it-through"]
|
||||
no_backup = true
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
|
||||
Reference in New Issue
Block a user