♻️ refactor(cook-it-through): move workflow engine into skill

This commit is contained in:
csh
2026-08-20 17:02:34 +08:00
parent f6854d065a
commit 9b95bf2682
19 changed files with 3905 additions and 1474 deletions
+1196 -181
View File
File diff suppressed because it is too large Load Diff
+425
View File
@@ -0,0 +1,425 @@
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MAIN_LOOP_SCRIPTS = ROOT / "skills" / "cook-it-through" / "scripts"
sys.path.insert(0, str(MAIN_LOOP_SCRIPTS))
from main_loop_scheduler import (
FeatureId,
FeatureRecord,
FeatureIntegrationId,
Scheduler,
SchedulerError,
TicketId,
TicketRecord,
parse_dependencies,
)
class SchedulerIdentityTests(unittest.TestCase):
def test_dependency_parser_accepts_only_canonical_qualified_identities(self):
owner = TicketId.parse("feature-a/04")
self.assertEqual(parse_dependencies("None", owner), ())
self.assertEqual(
parse_dependencies(
"feature-a/01; feature-b/03; feature-c@integrated",
owner,
),
(
TicketId.parse("feature-a/01"),
TicketId.parse("feature-b/03"),
FeatureIntegrationId.parse("feature-c@integrated"),
),
)
self.assertEqual(str(FeatureId.parse("feature-a")), "feature-a")
self.assertEqual(str(TicketId.parse("feature-a/01")), "feature-a/01")
self.assertEqual(
str(FeatureIntegrationId.parse("feature-a@integrated")),
"feature-a@integrated",
)
invalid_values = (
"01",
"01 - First",
"feature-a/01, feature-b/03",
"None - can start immediately",
"feature-a/1",
"Feature-A/01",
)
for raw in invalid_values:
with self.subTest(raw=raw), self.assertRaisesRegex(
SchedulerError,
"invalid dependency",
):
parse_dependencies(raw, owner)
with self.assertRaisesRegex(
SchedulerError,
"duplicate dependency feature-a/01",
):
parse_dependencies("feature-a/01; feature-a/01", owner)
with self.assertRaisesRegex(
SchedulerError,
"self dependency feature-a/04",
):
parse_dependencies("feature-a/04", owner)
class GlobalSchedulerTests(unittest.TestCase):
def test_scheduler_rejects_an_invalid_ticket_status_with_qualified_identity(self):
feature = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="invalid",
status="done",
dependencies=(),
),
),
)
with self.assertRaisesRegex(
SchedulerError,
"alpha/01: invalid status done",
):
Scheduler((feature,))
def test_scheduler_rejects_impossible_integration_state(self):
unresolved_integrated = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="unfinished",
status="ready-for-agent",
dependencies=(),
),
),
integrated=True,
)
with self.assertRaisesRegex(
SchedulerError,
"alpha@integrated: unsatisfied ticket alpha/01",
):
Scheduler((unresolved_integrated,))
alpha = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="pending",
status="resolved",
dependencies=(),
),
),
)
beta = FeatureRecord(
id=FeatureId.parse("beta"),
tickets=(
TicketRecord(
id=TicketId.parse("beta/01"),
slug="done",
status="resolved",
dependencies=(),
),
),
integrated=True,
)
with self.assertRaisesRegex(
SchedulerError,
"beta@integrated: earlier integration is pending: alpha@integrated",
):
Scheduler((alpha, beta))
def test_claimable_tickets_use_queue_priority_without_a_feature_head_barrier(self):
alpha = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="alpha-ticket",
status="claimed",
dependencies=(),
),
),
)
beta = FeatureRecord(
id=FeatureId.parse("beta"),
tickets=(
TicketRecord(
id=TicketId.parse("beta/02"),
slug="later-number",
status="ready-for-agent",
dependencies=(),
),
TicketRecord(
id=TicketId.parse("beta/01"),
slug="earlier-number",
status="ready-for-agent",
dependencies=(),
),
),
)
scheduler = Scheduler((alpha, beta))
self.assertEqual(
scheduler.ticket_frontier,
(TicketId.parse("beta/01"), TicketId.parse("beta/02")),
)
def test_ticket_and_integration_dependencies_have_distinct_satisfaction_rules(self):
beta_id = FeatureId.parse("beta")
alpha_ticket = TicketRecord(
id=TicketId.parse("alpha/01"),
slug="consumer",
status="ready-for-agent",
dependencies=(
TicketId.parse("beta/01"),
FeatureIntegrationId.parse("beta@integrated"),
),
)
beta = FeatureRecord(
id=beta_id,
tickets=(
TicketRecord(
id=TicketId.parse("beta/01"),
slug="provider",
status="skipped",
dependencies=(),
),
),
)
alpha = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(alpha_ticket,),
)
before_integration = Scheduler((beta, alpha))
self.assertEqual(before_integration.ticket_frontier, ())
self.assertEqual(
before_integration.integration_dependencies(alpha_ticket.id),
(FeatureIntegrationId.parse("beta@integrated"),),
)
self.assertEqual(
before_integration.unsatisfied_dependencies(alpha_ticket.id),
(FeatureIntegrationId.parse("beta@integrated"),),
)
after_integration = Scheduler(
(
FeatureRecord(
id=beta.id,
tickets=beta.tickets,
integrated=True,
),
alpha,
)
)
self.assertEqual(after_integration.ticket_frontier, (alpha_ticket.id,))
resolved_only = Scheduler(
(
FeatureRecord(
id=beta.id,
tickets=(
TicketRecord(
id=TicketId.parse("beta/01"),
slug="provider",
status="resolved",
dependencies=(),
),
),
),
FeatureRecord(
id=alpha.id,
tickets=(
TicketRecord(
id=alpha_ticket.id,
slug=alpha_ticket.slug,
status=alpha_ticket.status,
dependencies=(TicketId.parse("beta/01"),),
),
),
),
)
)
self.assertEqual(resolved_only.ticket_frontier, (alpha_ticket.id,))
def test_graph_validation_reports_missing_targets_and_cross_feature_cycles(self):
missing_feature = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="missing-feature",
status="ready-for-agent",
dependencies=(TicketId.parse("missing/01"),),
),
),
)
with self.assertRaisesRegex(
SchedulerError,
"alpha/01: dependency feature not queued: missing",
):
Scheduler((missing_feature,))
beta = FeatureRecord(
id=FeatureId.parse("beta"),
tickets=(
TicketRecord(
id=TicketId.parse("beta/01"),
slug="present",
status="ready-for-agent",
dependencies=(),
),
),
)
missing_ticket = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="missing-ticket",
status="ready-for-agent",
dependencies=(TicketId.parse("beta/99"),),
),
),
)
with self.assertRaisesRegex(
SchedulerError,
"alpha/01: dependency ticket not found: beta/99",
):
Scheduler((alpha := missing_ticket, beta))
cycle_alpha = FeatureRecord(
id=alpha.id,
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="cycle-a",
status="ready-for-agent",
dependencies=(TicketId.parse("beta/02"),),
),
),
)
cycle_beta = FeatureRecord(
id=beta.id,
tickets=(
TicketRecord(
id=TicketId.parse("beta/02"),
slug="cycle-b",
status="ready-for-agent",
dependencies=(
FeatureIntegrationId.parse("alpha@integrated"),
),
),
),
)
with self.assertRaisesRegex(
SchedulerError,
"dependency cycle: alpha/01 -> beta/02 -> alpha@integrated -> alpha/01",
):
Scheduler((cycle_alpha, cycle_beta))
def test_ready_integration_frontier_does_not_hide_later_ticket_frontier(self):
alpha = FeatureRecord(
id=FeatureId.parse("alpha"),
tickets=(
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="done",
status="resolved",
dependencies=(),
),
),
)
beta = FeatureRecord(
id=FeatureId.parse("beta"),
tickets=(
TicketRecord(
id=TicketId.parse("beta/01"),
slug="independent",
status="ready-for-agent",
dependencies=(),
),
),
)
scheduler = Scheduler((alpha, beta))
self.assertEqual(
scheduler.integration_frontier,
FeatureIntegrationId.parse("alpha@integrated"),
)
self.assertEqual(
scheduler.ticket_frontier,
(TicketId.parse("beta/01"),),
)
self.assertEqual(scheduler.feature_state(alpha.id), "ready-to-integrate")
self.assertEqual(scheduler.feature_state(beta.id), "queued")
def test_ticket_input_order_does_not_change_frontier_or_error_order(self):
tickets = (
TicketRecord(
id=TicketId.parse("alpha/10"),
slug="ten",
status="ready-for-agent",
dependencies=(),
),
TicketRecord(
id=TicketId.parse("alpha/02"),
slug="two",
status="ready-for-agent",
dependencies=(),
),
)
forward = Scheduler(
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=tickets),)
)
reverse = Scheduler(
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=tickets[::-1]),)
)
expected = (TicketId.parse("alpha/02"), TicketId.parse("alpha/10"))
self.assertEqual(forward.ticket_frontier, expected)
self.assertEqual(reverse.ticket_frontier, expected)
invalid_tickets = (
TicketRecord(
id=TicketId.parse("alpha/02"),
slug="second-error",
status="ready-for-agent",
dependencies=(TicketId.parse("missing/02"),),
),
TicketRecord(
id=TicketId.parse("alpha/01"),
slug="first-error",
status="ready-for-agent",
dependencies=(TicketId.parse("missing/01"),),
),
)
messages = []
for order in (invalid_tickets, invalid_tickets[::-1]):
with self.assertRaises(SchedulerError) as raised:
Scheduler(
(FeatureRecord(id=FeatureId.parse("alpha"), tickets=order),)
)
messages.append(str(raised.exception))
self.assertEqual(
messages,
[
"alpha/01: dependency feature not queued: missing",
"alpha/01: dependency feature not queued: missing",
],
)
if __name__ == "__main__":
unittest.main()
+228 -11
View File
@@ -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",
+440 -336
View File
@@ -8,9 +8,23 @@ 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", ROOT / "scripts" / "main_loop.py"
"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)
@@ -36,6 +50,19 @@ def required_flags(parser: argparse.ArgumentParser) -> set[str]:
}
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
@@ -59,6 +86,32 @@ 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]
@@ -112,8 +165,10 @@ LEGACY_FLOW_TERMS = (
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"))
[
*(path.read_text(encoding="utf-8") for path in sorted(TEMPLATES.rglob("*.md"))),
main_loop_bundle_text(),
]
)
for required in (
@@ -136,7 +191,8 @@ class TemplateContractsTests(unittest.TestCase):
self.assertNotIn("docs/workflows/", templates_readme)
self.assertNotIn("templates/workflows/", templates_readme)
self.assertNotIn("docs/superpowers/", templates_readme)
self.assertIn(".scratch/<feature>/spec.md", 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(
@@ -206,6 +262,44 @@ class TemplateContractsTests(unittest.TestCase):
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")
@@ -219,67 +313,153 @@ class TemplateContractsTests(unittest.TestCase):
},
)
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
stable_paths = (
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",
)
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)
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_template_defines_ticket_and_integration_contracts(self):
def test_agent_rules_routes_main_loop_work_to_the_firstparty_skill(self):
rules = rules_text()
normalized = " ".join(rules.split())
normalized = normalized_prose(rules)
self.assertIn("{{PLAYBOOK_ROOT}}", rules)
self.assertIn("{{PLAYBOOK_SCRIPTS}}", rules)
for heading in (
"## 任务入口",
"## 正式工程主链",
"## On-ramps 与 detours",
"## Phase boundaries",
"## 本地 Ticket 执行协议",
"## 文档职责",
"## 调度语义",
"## 执行隔离",
"## 主循环命令",
"## Git 与证据门禁",
"## 辅助能力",
"## Session 收尾",
):
self.assertIn(heading, rules, msg=f"missing section: {heading}")
for invariant in (
"`.scratch/queue.md`",
"多个 frontier tickets 可在 worktree 模式并发执行",
"只有 `reclaim` 可以接管",
"Standards/Spec 双轴审查",
"三个独立门禁,不能互相替代",
"跨机器或独立 clone",
"共享同一文件系统",
):
self.assertIn(invariant, normalized, msg=f"missing invariant: {invariant}")
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 (
@@ -288,40 +468,63 @@ class TemplateContractsTests(unittest.TestCase):
"/.scratch/**/*.tmp",
):
self.assertIn(runtime_rule, template)
self.assertNotIn("是否纳入版本控制由项目决定", rules_text())
rules = rules_text()
self.assertNotIn("是否纳入版本控制由项目决定", rules)
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_agent_rules_routes_each_current_matt_on_ramp_to_its_destination(self):
rules = rules_text()
on_ramps = section(rules, "## On-ramps 与 detours", "## Phase boundaries")
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.assertNotIn("`prototype`", on_ramps)
bug_route = section(rules, "### 入口 3", "### 入口 4")
self.assertIn("`diagnosing-bugs` 完整执行 Phase 1-6", bug_route)
self.assertNotIn("to-spec", bug_route)
self.assertIn(
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",
bug_route,
msg="diagnosing-bugs hands off to improve-codebase-architecture after "
"the fix lands, not to a design session before it",
)
self.assertNotIn(
"grill-with-docs",
bug_route,
msg="stopping a half-fixed defect to run a design session contradicts "
"the skill's own phase order",
)
):
self.assertIn(required, workflow)
self.assertNotIn("`<fixed-point>...HEAD`", workflow)
bug = section(workflow, "## 入口 3", "## 完成与升级")
self.assertNotIn("grill-with-docs", bug)
def test_agent_rules_commits_planning_baseline_before_claim(self):
rules = rules_text()
main_chain = section(rules, "## 正式工程主链", "## On-ramps 与 detours")
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",
@@ -329,58 +532,28 @@ class TemplateContractsTests(unittest.TestCase):
"-> 提交 planning baseline",
"-> main_loop.py claim",
)
positions = [main_chain.index(step) for step in ordered_steps]
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, main_chain)
self.assertIn("任何 `claim` 之", main_chain)
self.assertIn("不隐式提交", main_chain)
self.assertIn(durable_input, planning)
self.assertIn("任何 claim ", planning)
self.assertIn("不隐式提交", planning)
def test_agent_rules_commits_final_workflow_state_after_integration(self):
rules = rules_text()
main_chain = section(rules, "## 正式工程主链", "## On-ramps 与 detours")
integration = section(rules, "### Feature 顺序集成", "## Git 与证据门禁")
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)
self.assertLess(
main_chain.index("-> main_loop.py integrate"),
main_chain.index("-> 提交 final workflow state"),
)
for durable_path in (
"`.scratch/<feature>/`",
"`.scratch/queue.md`",
"`.scratch/<feature>/.main-loop.json`",
):
self.assertIn(durable_path, integration)
self.assertIn("不得用\n`git add .scratch`", integration)
self.assertIn("不得 amend 或 squash", integration)
self.assertIn("`MAIN_INTEGRATION_COMMIT`", integration)
def test_agent_rules_defines_a_local_ticket_execution_adapter(self):
rules = rules_text()
main_flow = section(rules, "## 正式工程主链", "## On-ramps 与 detours")
adapter = section(rules, "## 本地 Ticket 执行协议", "## 文档职责")
self.assertIn("本地 ticket 执行协议", main_flow)
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_binds_state_and_evidence_to_claimed_git_context(self):
rules = rules_text()
claim = section(rules, "### 领取", "### 心跳和接管")
commands = " ".join(section(rules, "## 主循环命令", "## Git 与证据门禁").split())
# claim's contract is its output keys and what each one addresses.
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",
@@ -391,128 +564,103 @@ class TemplateContractsTests(unittest.TestCase):
"BASE",
"ISOLATION",
):
self.assertIn(f"`{key}`", claim, msg=f"claim output key undocumented: {key}")
self.assertNotIn(
"--state-root .scratch",
claim.split("stdout 返回", 1)[1],
msg="after a claim, state must be addressed by the absolute STATE_ROOT",
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",
)
# Evidence has to be bound to the claimed commits, not to free text.
for binding in (
"--review-base <同一feature HEAD>",
"--verified \"<ticket-verification.json>\"",
"--reviewed \"<ticket-review.json>\"",
"--verified \"<feature-verification.json>\"",
"--main-verified \"<main-candidate-verification.json>\"",
"--reviewed \"<feature-review.json>\"",
"把该 `FEATURE_HEAD` 合入 ticket branch",
):
self.assertIn(binding, commands, msg=f"missing evidence binding: {binding}")
def test_agent_rules_defines_mechanical_review_inputs_and_pass_mapping(self):
rules = rules_text()
review = " ".join(
section(rules, "### Review 适配契约", "### Ticket 完成或状态转换").split()
)
positions = [execution.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
for required in (
"fixed point",
"`.scratch/<feature>/spec.md`",
"`.scratch/<feature>/issues/<ticket>-*.md`",
"`<STATE_ROOT>/<feature>/spec.md`",
"`<STATE_ROOT>/<feature>/issues/<NN>-*.md`",
"零个未解决的硬 finding",
"不得据此填写 `standards=pass` 或 `spec=pass`",
):
self.assertIn(required, review)
self.assertIn(
"不给 pass/fail 判定",
review,
msg="code-review emits findings only; the pass mapping is this "
"adapter's own layer and must not be presented as the skill's verdict",
"不得填写 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_agent_rules_reads_claimed_context_after_claim_and_defines_lease_policy(self):
rules = rules_text()
startup = section(rules, "## 会话启动", "## 任务入口")
claim = section(rules, "### 领取", "### 心跳和接管")
lease = section(rules, "### 心跳和接管", "### Review 适配契约")
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_orders_task_entries_by_cost_with_upgrade_conditions(self):
rules = rules_text()
entries = section(rules, "## 任务入口", "## 正式工程主链")
entry_headings = [h for h in headings(entries) if h.startswith("入口 ")]
self.assertEqual(
entry_headings,
[
"入口 1:直接执行",
"入口 2:单切片改动",
"入口 3:已明确预期行为的 bug",
"入口 4:新 feature 或设计变更",
],
msg="entries must stay ordered cheapest-first so the router can take "
"the first match",
)
self.assertEqual(
entries.count("**升级条件**"),
2,
msg="entry 1 and entry 2 each need an explicit upgrade trigger; "
"without one the router has no defined way out of a light path",
)
entry_two = section(entries, "### 入口 2", "### 入口 3")
entry_four = section(entries, "### 入口 4", "### 非交互模式下的入口 4")
self.assertIn(
"不属于入口 3",
entry_two,
msg="known bugs must reach diagnosing-bugs before the generic slice path",
)
self.assertIn("入口 4", entry_two, msg="entry 2 must name its escalation target")
self.assertIn(
"边界不清时先按入口 2 起步",
entry_four,
msg="an uncertain boundary must start at the single-slice path, not "
"pre-pay the full chain",
)
def test_agent_rules_gives_unattended_sessions_a_pre_ticket_fallback(self):
rules = rules_text()
fallback = section(
rules, "### 非交互模式下的入口 4", "## 正式工程主链"
)
self.assertIn("--result blocked", fallback)
self.assertIn(
"to-questionnaire",
fallback,
msg="grilling needs a user, so a ticketless unattended session must "
"have a defined way to hand questions back",
)
self.assertIn(".scratch/questions/<slug>.md", fallback)
self.assertIn("从 `grill-with-docs` 恢复", " ".join(fallback.split()))
def test_agent_rules_keeps_seam_confirmation_with_to_spec_and_tdd(self):
rules = rules_text()
main_flow = section(rules, "## 正式工程主链", "## On-ramps 与 detours")
self.assertIn("`tdd` 不得在未经确认的 seam 上开始", main_flow)
self.assertIn(
"seam confirmation 的责任在 `to-spec` 与 `tdd`",
main_flow,
msg="the grilling skills never mention seams, so the rules must not "
"route seam confirmation through them",
)
def test_agent_rules_orders_the_phase_boundary_options(self):
rules = rules_text()
phase_boundaries = section(rules, "## Phase boundaries", "## 本地 Ticket")
def test_cook_it_through_orders_phase_boundary_options_and_reload(self):
boundary = normalized_prose(main_loop_session_boundary_text())
ordered_options = (
"继续当前 session",
"使用 `clear`",
@@ -520,64 +668,57 @@ class TemplateContractsTests(unittest.TestCase):
"交给 subagent",
"使用 `compact`",
)
positions = [phase_boundaries.index(option) for option in ordered_options]
positions = [boundary.index(option) for option in ordered_options]
self.assertEqual(positions, sorted(positions))
self.assertIn("150k", phase_boundaries)
self.assertIn("`handoff` 解决的是可移植性", phase_boundaries)
def test_agent_rules_documents_every_main_loop_subcommand_and_required_flag(self):
rules = rules_text()
parsers = subcommand_parsers()
for command, parser in parsers.items():
self.assertIn(
f"main_loop.py {command}",
rules,
msg=f"undocumented subcommand: {command}",
)
for flag in required_flags(parser):
self.assertIn(
flag, rules, msg=f"undocumented required flag: {command} {flag}"
)
documented = set(re.findall(r"main_loop\.py ([a-z][a-z-]*)", rules))
self.assertEqual(
documented - set(parsers),
set(),
msg="the rules document subcommands the CLI does not expose",
)
def test_agent_rules_documents_executable_state_transition_commands(self):
rules = rules_text()
lease = section(rules, "### 心跳和接管", "### Review 适配契约")
transitions = section(
rules, "### Ticket 完成或状态转换", "### Feature 顺序集成"
)
self.assertNotIn("finish --result released|blocked", lease)
self.assertIn("--result blocked --reason", transitions)
self.assertIn("--result released", transitions)
self.assertIn("其他 `finish` 转换共用", transitions)
release_ticket = transitions.split("main_loop.py release-ticket", 1)[1]
self.assertNotIn("--repo-root", release_ticket)
self.assertNotIn("--owner", release_ticket)
def test_agent_rules_requires_snapshotted_evidence_artifacts(self):
rules = rules_text()
gate = " ".join(section(rules, "## Git 与证据门禁", "## 辅助能力").split())
for required in (
"UTF-8 JSON artifact",
"output_sha256",
"report_sha256",
".scratch/<feature>/evidence/",
"无法证明命令真的执行过",
"下一阶段需要当前 session 作为 primary source",
"约 150k tokens",
"`handoff` 解决的是可移植性",
"重新加载 `SKILL.md` 与当前路由文件",
"不得只依据 `status` 输出继续",
"`domain-modeling`",
"`codebase-design` 只作词汇来源",
):
self.assertIn(required, gate)
self.assertIn(required, boundary)
def test_agent_rules_only_references_installed_skills(self):
rules = rules_text()
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())
@@ -587,54 +728,17 @@ class TemplateContractsTests(unittest.TestCase):
)
referenced = {
token
for token in re.findall(r"`([a-z][a-z0-9-]+)`", rules)
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), [])
self.assertTrue(referenced, msg="expected the rules to reference skills")
self.assertEqual(
sorted(referenced - skills),
[],
msg="the rules reference skills that are not installed under skills/",
)
def test_agent_rules_states_what_the_evidence_gate_cannot_check(self):
rules = rules_text()
gate = section(rules, "## Git 与证据门禁", "## 辅助能力")
self.assertIn("三个独立门禁,不能互相替代", gate)
self.assertIn(
"无法",
gate,
msg="the gate binds evidence to real commits but cannot prove a test "
"run happened; the rules must say so instead of implying enforcement",
)
self.assertIn("--main-verified", gate)
def test_agent_rules_documents_the_recovery_path_for_stuck_tickets(self):
rules = rules_text()
recovery = section(rules, "### 卡死与恢复", "## 执行隔离")
self.assertIn("release-ticket", recovery)
self.assertIn(
"`reclaim` 只接管 `claimed`",
recovery,
msg="reclaim cannot rescue a blocked ticket; the rules must point at "
"the command that can",
)
self.assertIn("BASE", recovery)
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]
normalized = " ".join(isolation.split())
self.assertIn("`main_loop.py` 只支持 local Markdown tracker", normalized)
self.assertIn("没有 远程 tracker adapter", normalized)
self.assertIn("跨机器或独立 clone 的并发不受支持", normalized)
self.assertNotIn("必须改用具备远程", normalized)
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__":