♻️ refactor(cook-it-through): move workflow engine into skill
This commit is contained in:
+1196
-181
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user