feat(ack): add structured worker model routing

This commit is contained in:
2026-07-31 23:34:13 +08:00
parent ee66dbe9ce
commit ae3bce7b5d
23 changed files with 6491 additions and 327 deletions
+185
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
import copy
import importlib.util
import json
import re
import subprocess
import sys
import tempfile
@@ -12,6 +15,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
VALIDATOR = REPO_ROOT / "skills" / "ack" / "scripts" / "validate_tasks.py"
EXAMPLE = REPO_ROOT / "skills" / "ack" / "examples" / "tasks.example.yaml"
SCHEMA = REPO_ROOT / "skills" / "ack" / "templates" / "tasks.schema.json"
def valid_knowledge_board() -> dict:
@@ -59,6 +63,21 @@ def valid_knowledge_board() -> dict:
}
def valid_manual_routing_board() -> dict:
board = valid_knowledge_board()
board["ackVersion"] = "0.10.0"
board["project"]["orchestration"] = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
}
board["workerReceipts"] = []
return board
class AckTaskValidationTests(unittest.TestCase):
def run_validator(
self,
@@ -129,6 +148,160 @@ class AckTaskValidationTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("任务板校验通过", result.stdout)
def test_v010_requires_structured_routing_but_v009_remains_readable(self) -> None:
current = valid_knowledge_board()
current["ackVersion"] = "0.10.0"
self.assert_board_rejected_in_all_modes(
current,
"project.orchestration: is required",
)
legacy = valid_knowledge_board()
legacy["ackVersion"] = "0.9.0"
self.assert_board_accepted_in_all_modes(legacy)
def test_builtin_rejects_invalid_ack_semver_in_all_modes(self) -> None:
for invalid in ("0.10", "00.10.0", "0.10.01", "v0.10.0", "0.10.0-"):
with self.subTest(ack_version=invalid):
board = valid_knowledge_board()
board["ackVersion"] = invalid
self.assert_board_rejected_in_all_modes(
board,
"ackVersion 必须是合法 SemVer",
)
def test_schema_declares_semver_routing_and_attempt_contracts(self) -> None:
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
ack_pattern = re.compile(schema["properties"]["ackVersion"]["pattern"])
for valid in ("0.9.0", "0.10.0", "0.11.2-alpha.1+build.7", "1.0.0"):
with self.subTest(valid_semver=valid):
self.assertIsNotNone(ack_pattern.fullmatch(valid))
for invalid in ("0.10", "00.10.0", "0.10.01", "v0.10.0", "0.10.0-"):
with self.subTest(invalid_semver=invalid):
self.assertIsNone(ack_pattern.fullmatch(invalid))
current_gate, orchestration_gate, receipts_gate = schema["allOf"]
current_pattern = re.compile(
current_gate["if"]["properties"]["ackVersion"]["pattern"]
)
for current in ("0.10.0", "0.99.1", "1.0.0", "12.34.56+build"):
self.assertIsNotNone(current_pattern.search(current))
self.assertIsNone(current_pattern.search("0.9.99"))
self.assertIn("workerReceipts", current_gate["then"]["required"])
self.assertIn(
"workerReceipts",
current_gate["then"]["properties"],
)
self.assertIn(
"orchestration",
current_gate["then"]["properties"]["project"]["required"],
)
self.assertIn(
"orchestration",
current_gate["then"]["properties"]["project"]["properties"],
)
self.assertIn("workerReceipts", orchestration_gate["then"]["required"])
self.assertIn(
"workerReceipts",
orchestration_gate["then"]["properties"],
)
self.assertIn(
"orchestration",
receipts_gate["then"]["properties"]["project"]["required"],
)
self.assertIn(
"orchestration",
receipts_gate["then"]["properties"]["project"]["properties"],
)
for gate in (current_gate, orchestration_gate, receipts_gate):
self.assertEqual(
gate["then"]["properties"]["tasks"]["$ref"],
"#/definitions/launchableTasks",
)
role_dispatch = schema["definitions"]["roleDispatch"]
self.assertIn("attemptId", role_dispatch["required"])
receipt_rule = role_dispatch["allOf"][0]
self.assertEqual(
receipt_rule["then"]["properties"]["attemptId"]["type"],
"null",
)
self.assertEqual(
receipt_rule["else"]["properties"]["attemptId"]["type"],
"string",
)
@unittest.skipUnless(
importlib.util.find_spec("jsonschema") is not None,
"jsonschema is required for the schema-only contract test",
)
def test_schema_alone_enforces_v010_and_mutual_routing_presence(self) -> None:
import jsonschema # type: ignore
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
validator = jsonschema.Draft7Validator(schema)
def messages(board: dict) -> list[str]:
return [error.message for error in validator.iter_errors(board)]
base = {
"version": 1,
"ackVersion": "0.9.0",
"project": {"name": "demo"},
"tasks": [],
}
self.assertEqual(messages(base), [])
for current in ("0.10.0", "0.11.2-alpha.1+build.7", "1.0.0"):
with self.subTest(ack_version=current):
board = copy.deepcopy(base)
board["ackVersion"] = current
errors = messages(board)
self.assertIn("'workerReceipts' is a required property", errors)
self.assertIn("'orchestration' is a required property", errors)
manual = {
"profileVersion": 1,
"mode": "manual",
"allowedWorktrees": [],
"modelAllowlist": {},
"profiles": {},
"defaults": {},
}
orchestration_only = copy.deepcopy(base)
orchestration_only["project"]["orchestration"] = manual
self.assertIn(
"'workerReceipts' is a required property",
messages(orchestration_only),
)
receipts_only = copy.deepcopy(base)
receipts_only["workerReceipts"] = []
self.assertIn(
"'orchestration' is a required property",
messages(receipts_only),
)
complete = copy.deepcopy(orchestration_only)
complete["ackVersion"] = "0.10.0+routing.1"
complete["workerReceipts"] = []
self.assertEqual(messages(complete), [])
def test_v010_task_ids_match_launcher_while_legacy_ids_remain_readable(
self,
) -> None:
current = valid_manual_routing_board()
current["tasks"][0]["id"] = "BUG/001"
self.assert_board_rejected_in_all_modes(
current,
"v0.10 自动路由 id 只允许字母、数字、点、下划线和连字符",
)
legacy = valid_knowledge_board()
legacy["ackVersion"] = "0.9.0"
legacy["tasks"][0]["id"] = "BUG/001"
self.assert_board_accepted_in_all_modes(legacy)
def test_knowledge_applied_and_checks_must_reference_selected_knowledge(self) -> None:
result = self.run_validator(
"""
@@ -582,6 +755,18 @@ class AckTaskValidationTests(unittest.TestCase):
"T-RESOLUTION.resolution: 必须是对象",
)
def test_dispatch_rejects_free_command_field_without_jsonschema(self) -> None:
board = valid_knowledge_board()
board["tasks"][0]["dispatch"] = {
"command": "cursor-agent --yolo",
"rounds": [],
}
self.assert_board_rejected_in_all_modes(
board,
"dispatch: 未知字段 'command'",
)
def test_valid_optional_schema_fields_pass_in_all_modes(self) -> None:
board = valid_knowledge_board()
board.update(