feat(tasks): add skill-templated task graph execution

This commit is contained in:
2026-06-23 10:22:58 +08:00
parent 6843d89b2c
commit 53b13e8eac
53 changed files with 4773 additions and 756 deletions

View File

@ -28,6 +28,7 @@ from .utils import (
check_requirements,
escape_xml,
extract_required_tool_names,
extract_skill_team_template,
get_missing_requirements,
parse_frontmatter,
parse_skill_metadata_blob,
@ -49,6 +50,8 @@ class SkillRecord:
tool_hints: list[str] = field(default_factory=list)
frontmatter: dict[str, Any] = field(default_factory=dict)
description: str = ""
team_template: dict[str, Any] | None = None
team_template_warnings: list[str] = field(default_factory=list)
class SkillsLoader:
@ -113,6 +116,7 @@ class SkillsLoader:
continue
normalized_frontmatter = dict(frontmatter)
meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", ""))
template_result = extract_skill_team_template(body)
record = SkillRecord(
name=name,
path=skill_file,
@ -127,6 +131,8 @@ class SkillsLoader:
),
frontmatter=normalized_frontmatter,
description=str(frontmatter.get("description") or summarize_body(body) or name),
team_template=template_result.template,
team_template_warnings=template_result.warnings,
)
if filter_unavailable and not self._record_available(record):
continue
@ -146,6 +152,7 @@ class SkillsLoader:
else:
path = self.workspace_skills / name / "versions" / loaded.version.version / "SKILL.md"
_frontmatter, body = parse_frontmatter(loaded.content)
template_result = extract_skill_team_template(body)
record = SkillRecord(
name=name,
path=path,
@ -160,6 +167,8 @@ class SkillsLoader:
),
frontmatter=dict(loaded.version.frontmatter),
description=str(loaded.version.frontmatter.get("description") or loaded.version.summary or name),
team_template=template_result.template,
team_template_warnings=template_result.warnings,
)
if filter_unavailable and not self._record_available(record):
continue

View File

@ -17,6 +17,7 @@ import json
import os
import re
import shutil
from dataclasses import dataclass, field
from typing import Any
@ -84,6 +85,27 @@ def strip_frontmatter(content: str) -> str:
return body
@dataclass(slots=True)
class SkillTeamTemplateParseResult:
template: dict[str, Any] | None = None
warnings: list[str] = field(default_factory=list)
def extract_skill_team_template(body: str) -> SkillTeamTemplateParseResult:
matches = re.findall(r"```beaver-team-template\s*\n(.*?)\n```", body, re.DOTALL)
if not matches:
return SkillTeamTemplateParseResult()
if len(matches) != 1:
return SkillTeamTemplateParseResult(warnings=["skill defines multiple team templates"])
try:
template = json.loads(matches[0])
except json.JSONDecodeError:
return SkillTeamTemplateParseResult(warnings=["team template JSON is invalid"])
if not isinstance(template, dict) or not isinstance(template.get("nodes", []), list):
return SkillTeamTemplateParseResult(warnings=["team template must be an object with a nodes list"])
return SkillTeamTemplateParseResult(template=template)
def extract_required_tool_names(body: str) -> list[str]:
"""从 canonical skill 正文的 `## Required Tools` 段落提取工具名。