
Skill Forge
- 1 installs
- 6 repo stars
- Updated April 13, 2026
- lanyasheng/auto-improvement-orchestrator-skill
Auto-generates a task_suite.yaml test harness for an existing SKILL.md, or a full SKILL.md plus tests from a skill_spec.yaml, with null-skill calibration.
About
Generates evaluation task suites from a SKILL.md by extracting testable claims from five sections and assigning judge types, or scaffolds a new skill from a spec. A developer uses it to add runnable tests to a skill or generate one from requirements.
- Extracts 5-10 tasks from frontmatter, When to Use, examples, anti-examples, and output format
- Null-skill calibration filters out tasks the base LLM passes without the skill loaded
Skill Forge by the numbers
- 1 all-time installs (skills.sh)
- Ranked #642 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lanyasheng/auto-improvement-orchestrator-skill --skill skill-forgeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 6 |
| Last updated | April 13, 2026 |
| Repository | lanyasheng/auto-improvement-orchestrator-skill ↗ |
What it does
Auto-generates a task_suite.yaml test harness for an existing SKILL.md, or a full SKILL.md plus tests from a skill_spec.yaml, with null-skill calibration.
Files
Skill Forge
Generate Skills from requirements AND generate task_suite.yaml for evaluation.
The primary value of this skill is task_suite generation -- turning a SKILL.md into a structured test harness that improvement-evaluator can run. Secondary value is generating SKILL.md from a structured skill_spec.yaml.
Key differentiator: Skill Forge does not merely scaffold a skeleton; it performs static analysis of the SKILL.md to extract testable claims (from five distinct sources) and assigns the appropriate judge type per task, producing a suite that is immediately runnable by improvement-evaluator.
When to Use
1. Add tests to an existing Skill -- You have a SKILL.md but no task_suite.yaml. Run --from-skill to analyze the SKILL.md and generate a test suite automatically. The generator extracts scenarios from five sections of the document and assigns the right judge per task. 2. Create a new Skill from scratch -- You have a skill_spec.yaml describing what the skill should do. Run --from-spec to generate both a complete SKILL.md (with frontmatter, sections, examples) and a matching task_suite.yaml. 3. Full lifecycle -- Generate a skill, test it with --evaluate, and optionally improve it with --auto-improve in one pass. Combines skill-forge, improvement-evaluator, and improvement-orchestrator into a single command. 4. Validate coverage of an existing suite -- Compare the generated task_suite against a hand-written one to find untested scenarios.
When NOT to Use
- You just want to evaluate an existing skill with an existing
task_suite.yaml
→ use improvement-evaluator
- You just want to improve a skill that already has tests
→ use improvement-orchestrator
- You want to manually write a SKILL.md with templates
→ use skill-creator
- You want to score improvement candidates (not generate tests)
→ use improvement-discriminator
- You want to merge overlapping skills into one consolidated skill
→ use skill-distill
Modes
Mode A: --from-skill (Generate task suite for existing SKILL.md)
# Generate test suite for an existing skill
python3 scripts/forge.py --from-skill /path/to/skill-dir --output /path/to/output
# Generate and immediately evaluate
python3 scripts/forge.py --from-skill /path/to/skill-dir --output /path/to/output --evaluateReads the SKILL.md, extracts scenarios from five sources in priority order:
1. Frontmatter (description, triggers) → 1 core-capability test 2. "When to Use" bullets → up to 3 positive use-case tests 3. `<example>` tags → up to 2 keyword-match tests (uses ContainsJudge) 4. `<anti-example>` tags → up to 2 negative tests (should avoid bad patterns) 5. Output format/CLI sections → 1 format compliance test
Produces task_suite.yaml in the output directory. Example generated output:
skill_id: release-notes-generator
version: "1.0"
generated_by: skill-forge
tasks:
- id: release-notes-generator-core-capability
description: "Test core capability described in skill description"
prompt: "You are an AI assistant with this skill loaded..."
judge:
type: llm-rubric
rubric: "The output should demonstrate the capability..."
pass_threshold: 0.7
timeout_seconds: 120
source: frontmatter.description
- id: release-notes-generator-use-case-01
description: "Use case: Generate notes from git log between tags"
prompt: "Scenario: Generate notes from git log..."
judge:
type: llm-rubric
rubric: "The output should address this use case..."
pass_threshold: 0.6
timeout_seconds: 120
source: when_to_useMode B: --from-spec (Generate skill + task suite from spec)
# Generate complete skill from a spec file
python3 scripts/forge.py --from-spec spec.yaml --output /path/to/output
# Generate, evaluate, and auto-improve if below SOLID grade
python3 scripts/forge.py --from-spec spec.yaml --output /path/to/output --auto-improveReads a skill_spec.yaml (see references/spec-format.md) and generates:
1. A complete SKILL.md with frontmatter, When to Use / When NOT to Use, examples, output format 2. A task_suite.yaml derived from the generated SKILL.md (same five-source extraction)
The spec format requires only name and purpose; optional fields (inputs, outputs, quality_criteria, domain_knowledge, reference_skills) enrich the generated SKILL.md. Example minimal spec:
name: release-notes-generator
purpose: Generate structured release notes from git commit history
inputs:
- name: commits
type: git-log
description: "Git commit log between two tags"
outputs:
- name: release-notes
format: markdown
description: "Structured release notes with sections"
quality_criteria:
- name: completeness
description: "All commits accounted for in the notes"
weight: 0.3Common Flags
| Flag | Effect |
|---|---|
--mock | Use mock LLM (for testing without API calls) |
--evaluate | Run improvement-evaluator after generation (requires it installed) |
--auto-improve | Run improvement-orchestrator if score below SOLID (requires it installed) |
Output Artifacts
| Request | Deliverable | Location |
|---|---|---|
--from-skill | task_suite.yaml with 5-10 test tasks | <output>/task_suite.yaml |
--from-spec | SKILL.md + task_suite.yaml | <output>/<name>/SKILL.md, <output>/<name>/task_suite.yaml |
--evaluate | Evaluation report (pass/fail per task, aggregate pass rate) | stdout + <output>/evaluation_report.json |
--auto-improve | Improved SKILL.md (if score was below SOLID) | in-place update of SKILL.md |
All generated YAML files use allow_unicode: True and default_flow_style: False for human readability. Files are written atomically (write-then-rename) to prevent corruption on crash.
Task Generation Strategy
The generator extracts test scenarios from 5 sources in the SKILL.md:
1. Frontmatter description → 1 core-capability test 2. "When to Use" section → up to 3 positive use-case tests 3. `<example>` tags → up to 2 keyword-match tests 4. `<anti-example>` tags → up to 2 anti-pattern avoidance tests 5. Output format section → 1 format compliance test
Tasks are deduplicated and capped at 10 per suite.
Judge Selection
- Scenario with specific keywords/outputs →
ContainsJudge - Scenario requiring quality/style assessment →
LLMRubricJudge - Scenario with structured output →
PytestJudge(if test script can be generated)
Harness Pattern Tasks (for scripted skills)
When the target skill has a scripts/ directory, forge auto-generates additional test tasks checking execution-harness pattern adoption:
- Timeout handling: Does the skill handle
subprocess.TimeoutExpired? - Atomic writes: Does it use
write_json/write_textfromlib/common(atomic write-then-rename)? - Backup/rollback: Does it create backups before file modifications?
- Error escalation: Does it have graduated error handling (not just crash-on-first-failure)?
- State persistence: Does it write recoverable state for crash recovery?
These tasks use ContainsJudge to grep the skill's Python source code. They only apply to orchestration/tool-type skills — pure-text knowledge skills skip this category.
Why Null-Skill Calibration
A generated task suite is only useful if it measures the _skill's_ contribution, not the base LLM's general ability. Without calibration, a naive suite can report 80%+ pass rates even when the SKILL.md adds zero value -- because the LLM already knows how to answer those questions.
Null-skill calibration addresses this by running every candidate task against a "null skill" (empty context, no SKILL.md loaded). Any task the null skill passes trivially is filtered out before the final suite is emitted. This ensures that every surviving task genuinely requires the knowledge or structure encoded in the SKILL.md.
Tradeoff: Null-skill calibration adds one extra LLM call per candidate task (or a heuristic keyword check in --mock mode). For a typical 10-task candidate set, this means ~10 additional calls during suite generation. The cost is justified because an uncalibrated suite gives false confidence: a skill that scores 9/10 on easy tasks looks "SOLID" but may add no value over a bare model. Calibrated suites reliably distinguish genuine skill contributions from baseline LLM capability.
When --mock is used, calibration falls back to a heuristic: tasks whose prompt contains only generic verbs ("explain", "describe", "list") without skill-specific terminology are filtered. This is less precise than LLM-based calibration but costs zero API calls.
The calibration step runs after deduplication and before the final cap of 10 tasks per suite.
Related Skills
| Skill | Relationship | When to prefer over skill-forge |
|---|---|---|
improvement-evaluator | Downstream consumer: runs the generated task_suite.yaml and reports pass/fail per task | You already have both SKILL.md and task_suite.yaml, just need to run them |
improvement-orchestrator | Drives the full generate-evaluate-improve loop; skill-forge is one step in this loop | You want automatic multi-round improvement, not just test generation |
improvement-generator | Generates improvement candidates (patches) for a SKILL.md | You want to improve an existing skill's prose/structure, not generate tests |
improvement-discriminator | Scores improvement candidates via multi-reviewer blind panel | You need to judge which candidate patch is best |
skill-creator | Manual SKILL.md authoring guide with templates | You prefer hand-writing the SKILL.md rather than generating it |
skill-distill | Merges multiple overlapping skills into one distilled skill | You have redundant skills to consolidate, not a new skill to create |
"""Skill spec schema for --from-spec mode."""
from dataclasses import dataclass, field
from typing import Any
import yaml
from pathlib import Path
@dataclass
class SkillSpec:
"""Structured specification for generating a new Skill."""
name: str
purpose: str
inputs: list[dict] = field(default_factory=list)
outputs: list[dict] = field(default_factory=list)
quality_criteria: list[dict] = field(default_factory=list)
domain_knowledge: list[str] = field(default_factory=list)
reference_skills: list[str] = field(default_factory=list)
critical_constraints: list[str] = field(default_factory=list)
@classmethod
def from_yaml(cls, path: Path) -> "SkillSpec":
"""Load a SkillSpec from a YAML file."""
data = yaml.safe_load(path.read_text())
return cls(
**{k: v for k, v in data.items() if k in cls.__dataclass_fields__}
)
def validate(self) -> list[str]:
"""Return a list of validation errors (empty if valid)."""
errors = []
if not self.name:
errors.append("name is required")
if not self.purpose:
errors.append("purpose is required")
if len(self.name) > 50:
errors.append("name too long (max 50 chars)")
if not self.name.replace("-", "").replace("_", "").isalnum():
errors.append(
"name must contain only alphanumeric, hyphens, underscores"
)
return errors
skill-forge
Generate testable Skills and task suites from requirements or existing SKILL.md files.
What it does
skill-forge reads a SKILL.md (or a skill_spec.yaml) and produces a task_suite.yaml -- a structured set of 5-10 test tasks that improvement-evaluator can run to measure the skill's real-world effectiveness.
The generator extracts testable claims from five sections of the SKILL.md:
1. Frontmatter description and triggers 2. "When to Use" scenarios 3. <example> tags 4. <anti-example> tags 5. Output format specifications
Each extracted scenario is paired with the appropriate judge type (ContainsJudge, LLMRubricJudge, or PytestJudge).
Quick start
# Generate a test suite for an existing skill
python3 scripts/forge.py --from-skill ./my-skill --output ./output
# Generate a skill + tests from a spec
python3 scripts/forge.py --from-spec spec.yaml --output ./output
# Full pipeline: generate, evaluate, and auto-improve
python3 scripts/forge.py --from-spec spec.yaml --output ./output --auto-improveDirectory structure
skill-forge/
SKILL.md # Skill definition (loaded by Claude Code)
README.md # This file
scripts/
forge.py # CLI entry point
task_suite_generator.py # task_suite.yaml generation logic
skill_generator.py # SKILL.md generation from spec
interfaces/
spec_schema.py # SkillSpec dataclass + validation
references/
spec-format.md # skill_spec.yaml format documentation
examples/
code-review-spec.yaml
release-notes-spec.yaml
tests/
test_forge.py
test_skill_generator.py
test_task_suite_generator.pyKey concepts
- Null-skill calibration: Tasks that a bare LLM (no SKILL.md loaded) would pass trivially are filtered out, ensuring the suite measures the skill's actual contribution.
- Judge selection: The generator automatically picks the right judge type based on the scenario -- keyword matching for examples with specific outputs, LLM rubrics for quality assessments, pytest for structured outputs.
- Harness pattern tasks: For skills with a
scripts/directory, additional tasks check for timeout handling, atomic writes, backup/rollback, and error escalation patterns.
Related skills
| Skill | Role |
|---|---|
improvement-evaluator | Runs the generated task_suite.yaml |
improvement-orchestrator | Drives the generate-evaluate-improve loop |
improvement-generator | Produces improvement candidates for skills |
skill-creator | Manual SKILL.md authoring guide |
License
MIT
name: code-review-assistant
purpose: Perform structured code review with categorized findings and severity levels
inputs:
- name: diff
type: git-diff
description: "Git diff of changes to review"
- name: context
type: file-list
description: "Surrounding source files for context"
outputs:
- name: review-report
format: markdown
description: "Structured review with findings, severity, and suggestions"
- name: summary
format: text
description: "One-paragraph review summary with approval/changes-requested verdict"
quality_criteria:
- name: actionability
description: "Each finding has a specific, implementable suggestion"
weight: 0.3
- name: severity-accuracy
description: "Severity levels (critical/major/minor/nit) correctly assigned"
weight: 0.25
- name: coverage
description: "Review covers logic, style, security, and performance dimensions"
weight: 0.25
- name: false-positive-rate
description: "Minimal false positives (flagging correct code as wrong)"
weight: 0.2
domain_knowledge:
- "Use RUVERI categories: Readability, Understandability, Verifiability, Extensibility, Robustness, Integrity"
- "Critical = crash/data loss/security; Major = incorrect behavior; Minor = style/convention; Nit = preference"
- "Always check for null/undefined handling in dynamic languages"
- "Memory management issues are critical in C/C++ code"
reference_skills:
- code-review-enhanced
- static-analysis
- security-review
name: release-notes-generator
purpose: Generate structured release notes from git commit history
inputs:
- name: commits
type: git-log
description: "Git commit log between two tags"
- name: platform
type: enum
description: "Target platform (ios/android/harmony/all)"
outputs:
- name: release-notes
format: markdown
description: "Structured release notes with sections for features, fixes, refactoring"
quality_criteria:
- name: platform-isolation
description: "Notes only reference the target platform, no cross-platform leakage"
weight: 0.3
- name: categorization
description: "Commits correctly classified into feat/fix/refactor sections"
weight: 0.3
- name: completeness
description: "All commits accounted for in the notes"
weight: 0.2
- name: readability
description: "Human-readable descriptions, not just raw commit messages"
weight: 0.2
domain_knowledge:
- "NanoCompose has 4 platforms: iOS, Android, HarmonyOS, Core C++"
- "Conventional commit prefixes: feat, fix, refactor, perf, test, docs, chore"
- "Core C++ changes affect all platforms and should be noted per-platform"
- "Platform-specific files live under platforms/{ios,android,harmony}/"
reference_skills:
- changelog-gen
- code-review-enhanced
Skill Spec Format (skill_spec.yaml)
A structured YAML specification for generating a new Skill via --from-spec mode.
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Skill identifier (alphanumeric, hyphens, underscores; max 50 chars) |
purpose | string | One-sentence description of what the skill does |
Optional Fields
| Field | Type | Description |
|---|---|---|
inputs | list[{name, type, description}] | What the skill takes as input |
outputs | list[{name, format, description}] | What the skill produces |
quality_criteria | list[{name, description, weight}] | How to judge output quality |
domain_knowledge | list[string] | Key domain facts the skill should encode |
reference_skills | list[string] | Related skills for cross-references |
Example
name: release-notes-generator
purpose: Generate structured release notes from git commit history
inputs:
- name: commits
type: git-log
description: "Git commit log between two tags"
- name: platform
type: enum
description: "Target platform (ios/android/harmony/all)"
outputs:
- name: release-notes
format: markdown
description: "Structured release notes with sections"
quality_criteria:
- name: platform-isolation
description: "Notes only reference the target platform"
weight: 0.3
- name: categorization
description: "Commits correctly classified into feat/fix/refactor"
weight: 0.3
- name: completeness
description: "All commits accounted for in the notes"
weight: 0.2
- name: readability
description: "Human-readable, not just commit messages"
weight: 0.2
domain_knowledge:
- "NanoCompose has 4 platforms: iOS, Android, HarmonyOS, Core C++"
- "Conventional commit prefixes: feat, fix, refactor, perf, test, docs, chore"
- "Core C++ changes affect all platforms"
reference_skills:
- changelog-gen
- code-review-enhancedValidation Rules
1. name must be non-empty, max 50 chars, alphanumeric with hyphens/underscores 2. purpose must be non-empty 3. quality_criteria weights should sum to ~1.0 (not enforced but recommended) 4. inputs and outputs should each have a name field at minimum
#!/usr/bin/env python3
"""Skill Forge CLI — generate Skills and task suites.
Two primary modes:
Mode A: Generate task suite for existing skill
python3 scripts/forge.py --from-skill /path/to/skill --output /path/to/output
Mode B: Generate skill + task suite from spec
python3 scripts/forge.py --from-spec spec.yaml --output /path/to/output
Common flags:
--mock Use mock LLM (for testing)
--evaluate Run evaluator after generation (requires improvement-evaluator)
--auto-improve Run orchestrator if below SOLID (requires improvement-orchestrator)
"""
import argparse
import sys
import yaml
import json
from pathlib import Path
# Add parent directory to path so we can import sibling modules
_SCRIPT_DIR = Path(__file__).resolve().parent
_SKILL_DIR = _SCRIPT_DIR.parent
sys.path.insert(0, str(_SKILL_DIR))
from scripts.task_suite_generator import generate_task_suite, write_task_suite
from scripts.skill_generator import generate_skill_from_spec
from interfaces.spec_schema import SkillSpec
def main():
parser = argparse.ArgumentParser(
description="Skill Forge: generate Skills and task suites",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--from-skill",
type=Path,
help="Path to an existing skill directory (with SKILL.md). "
"Generates task_suite.yaml only.",
)
group.add_argument(
"--from-spec",
type=Path,
help="Path to a skill_spec.yaml. Generates SKILL.md + task_suite.yaml.",
)
parser.add_argument(
"--output",
type=Path,
help="Output directory for generated files. "
"Defaults to current directory.",
)
parser.add_argument(
"--mock",
action="store_true",
help="Use mock LLM (for testing without API calls).",
)
parser.add_argument(
"--evaluate",
action="store_true",
help="Run improvement-evaluator after generation.",
)
parser.add_argument(
"--auto-improve",
action="store_true",
help="Run improvement-orchestrator if score below SOLID.",
)
args = parser.parse_args()
# Determine output directory
output_dir = args.output or Path(".")
if args.from_skill:
# Mode A: Generate task suite for existing skill
return handle_from_skill(args.from_skill, output_dir, args)
elif args.from_spec:
# Mode B: Generate skill + task suite from spec
return handle_from_spec(args.from_spec, output_dir, args)
def handle_from_skill(
skill_path: Path, output_dir: Path, args: argparse.Namespace
) -> int:
"""Mode A: Generate task suite for an existing SKILL.md."""
skill_path = skill_path.resolve()
if not skill_path.is_dir():
print(f"Error: {skill_path} is not a directory", file=sys.stderr)
return 1
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
print(f"Error: No SKILL.md found at {skill_md}", file=sys.stderr)
return 1
print(f"Analyzing SKILL.md at {skill_path}...")
suite = generate_task_suite(skill_path, mock=args.mock)
task_count = len(suite.get("tasks", []))
print(f"Generated {task_count} test tasks.")
out_file = write_task_suite(suite, output_dir)
print(f"Task suite written to {out_file}")
if args.evaluate:
return run_evaluation(output_dir, suite["skill_id"])
return 0
def handle_from_spec(
spec_path: Path, output_dir: Path, args: argparse.Namespace
) -> int:
"""Mode B: Generate skill + task suite from a spec."""
spec_path = spec_path.resolve()
if not spec_path.exists():
print(f"Error: Spec file not found: {spec_path}", file=sys.stderr)
return 1
# Load and validate spec
spec = SkillSpec.from_yaml(spec_path)
errors = spec.validate()
if errors:
for e in errors:
print(f"Spec validation error: {e}", file=sys.stderr)
return 1
print(f"Generating skill '{spec.name}' from spec...")
# Create skill directory
skill_dir = output_dir / spec.name
skill_dir.mkdir(parents=True, exist_ok=True)
# Generate SKILL.md
spec_dict = yaml.safe_load(spec_path.read_text())
skill_md_content = generate_skill_from_spec(spec_dict)
skill_md_path = skill_dir / "SKILL.md"
skill_md_path.write_text(skill_md_content)
print(f"SKILL.md written to {skill_md_path}")
# Generate task suite from the generated SKILL.md
suite = generate_task_suite(skill_dir, mock=args.mock)
task_count = len(suite.get("tasks", []))
print(f"Generated {task_count} test tasks.")
out_file = write_task_suite(suite, skill_dir)
print(f"Task suite written to {out_file}")
if args.evaluate:
return run_evaluation(skill_dir, suite["skill_id"])
if args.auto_improve:
return run_auto_improve(skill_dir, suite["skill_id"])
return 0
def run_evaluation(output_dir: Path, skill_id: str) -> int:
"""Run improvement-evaluator on the generated task suite."""
evaluator_script = (
Path.home()
/ ".claude/skills/improvement-evaluator/scripts/evaluate.py"
)
if not evaluator_script.exists():
print(
"Warning: improvement-evaluator not found. "
"Skipping evaluation.",
file=sys.stderr,
)
return 0
import subprocess
result = subprocess.run(
[
sys.executable,
str(evaluator_script),
"--skill",
skill_id,
"--suite",
str(output_dir / "task_suite.yaml"),
],
capture_output=True,
text=True,
)
print(result.stdout)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
return result.returncode
def run_auto_improve(output_dir: Path, skill_id: str) -> int:
"""Run improvement-orchestrator if score is below SOLID."""
orchestrator_script = (
Path.home()
/ ".claude/skills/improvement-orchestrator/scripts/orchestrate.py"
)
if not orchestrator_script.exists():
print(
"Warning: improvement-orchestrator not found. "
"Skipping auto-improve.",
file=sys.stderr,
)
return 0
import subprocess
result = subprocess.run(
[
sys.executable,
str(orchestrator_script),
"--skill",
skill_id,
"--target-grade",
"SOLID",
],
capture_output=True,
text=True,
)
print(result.stdout)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
return result.returncode
if __name__ == "__main__":
sys.exit(main() or 0)
"""Generate SKILL.md from a skill_spec.yaml.
Takes a structured specification (name, purpose, inputs, outputs,
quality_criteria) and produces a complete SKILL.md with frontmatter,
When to Use / When NOT to Use sections, examples, and output artifacts.
"""
import yaml
from pathlib import Path
def generate_skill_from_spec(spec: dict) -> str:
"""Generate a complete SKILL.md from a structured spec.
Args:
spec: Dict with keys: name, purpose, inputs, outputs,
quality_criteria, domain_knowledge, reference_skills.
Returns:
A string containing the full SKILL.md content.
"""
name = spec["name"]
purpose = spec["purpose"]
# Frontmatter
frontmatter = {
"name": name,
"description": derive_description(purpose, spec.get("inputs", [])),
"license": "MIT",
"triggers": derive_triggers(name, purpose),
}
# Body sections
sections: list[str] = []
# Title and purpose
title = name.replace("-", " ").title()
sections.append(f"# {title}\n")
sections.append(f"{purpose}\n")
# When to Use
sections.append("## When to Use\n")
inputs = spec.get("inputs", [])
if inputs:
for inp in inputs:
desc = inp.get("description", inp.get("name", "input"))
sections.append(f"- When you have {desc}\n")
else:
sections.append(f"- When you need to {purpose.lower()}\n")
# When NOT to Use
sections.append("\n## When NOT to Use\n")
sections.append(
"- For general-purpose tasks not related to this skill's domain\n"
)
ref_skills = spec.get("reference_skills", [])
if ref_skills:
for ref in ref_skills[:2]:
sections.append(
f"- When you specifically need {ref} capabilities instead\n"
)
# Examples from quality_criteria (P7 behavior anchoring from prompt-hardening)
quality = spec.get("quality_criteria", [])
if quality:
criteria_desc = "; ".join(
c.get("description", c.get("name", ""))
for c in quality[:3]
)
sections.append("\n<example>\n")
sections.append(
f"Correct usage: Apply {name} to produce output meeting "
f"quality criteria: {criteria_desc}\n"
)
sections.append("reasoning: These criteria ensure the output is "
"usable without manual correction.\n")
sections.append("</example>\n")
sections.append("\n<anti-example>\n")
sections.append(
f"Incorrect: Producing output that violates quality criteria "
f"({criteria_desc})\n"
)
sections.append("reasoning: Violating these criteria means the output "
"needs manual rework, defeating the purpose of the skill.\n")
sections.append("</anti-example>\n")
# P1 Triple reinforcement for critical constraints
critical = spec.get("critical_constraints", [])
if critical:
sections.append("\n## Critical Constraints\n")
for constraint in critical:
sections.append(f"\nMUST: {constraint}\n")
sections.append(
f"\nI REPEAT: The above constraints are non-negotiable. "
f"NEVER skip or rationalize around them.\n"
)
# Domain knowledge
domain = spec.get("domain_knowledge", [])
if domain:
sections.append("\n## Domain Knowledge\n")
for item in domain:
sections.append(f"- {item}\n")
# Output section
outputs = spec.get("outputs", [])
if outputs:
sections.append("\n## Output Artifacts\n")
sections.append("| Request | Deliverable |\n")
sections.append("|---------|------------|\n")
for out in outputs:
fmt = out.get("format", "text")
out_name = out.get("name", "output")
sections.append(f"| {name} execution | {fmt}: {out_name} |\n")
# Related skills
if ref_skills:
sections.append("\n## Related Skills\n")
for ref in ref_skills:
sections.append(f"- `{ref}`\n")
# Compose final document
fm_str = yaml.dump(
frontmatter,
allow_unicode=True,
default_flow_style=False,
)
body = "".join(sections)
return f"---\n{fm_str}---\n\n{body}"
def derive_description(purpose: str, inputs: list) -> str:
"""Create a concise description from purpose and inputs."""
if inputs:
input_hints = ", ".join(
i.get("description", i.get("name", ""))[:30] for i in inputs[:2]
)
return f"{purpose}. Input: {input_hints}"
return purpose
def derive_triggers(name: str, purpose: str) -> list[str]:
"""Generate trigger phrases from the skill name and purpose."""
triggers = [name, name.replace("-", " ")]
# Extract meaningful words from purpose (>3 chars, not common)
stop_words = {
"that", "this", "with", "from", "into", "when", "will",
"have", "been", "more", "also", "they", "them", "than",
"each", "make", "like", "long", "very", "just", "over",
}
for word in purpose.split():
clean = word.strip(".,;:!?").lower()
if len(clean) > 3 and clean not in stop_words:
triggers.append(clean)
# Deduplicate while keeping order, cap at 6
seen: set[str] = set()
unique: list[str] = []
for t in triggers:
if t.lower() not in seen:
seen.add(t.lower())
unique.append(t)
return unique[:6]
"""Generate task_suite.yaml from an existing SKILL.md.
Strategy:
1. Parse SKILL.md frontmatter (name, description, triggers)
2. Extract key sections:
- "When to Use" scenarios -> positive test tasks
- "When NOT to Use" -> negative test tasks (should reject/redirect)
- <example> tags -> expected-output positive tests
- <anti-example> tags -> negative tests
- CLI/Usage sections -> command format tests
3. For each extracted scenario, generate a task with appropriate judge
4. Calibrate: filter out tasks that a null-skill (empty context) would pass
Judge selection logic:
- Scenario mentions specific keywords/outputs -> ContainsJudge
- Scenario involves quality/style assessment -> LLMRubricJudge
- Scenario has structured output -> PytestJudge (if test script can be generated)
"""
import re
import yaml
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Any
@dataclass
class GeneratedTask:
"""A single test task in a task suite."""
id: str
description: str
prompt: str
judge: dict # {type, expected/rubric/test_file, pass_threshold}
timeout_seconds: int = 120
source: str = "" # Which part of SKILL.md this came from
def generate_task_suite(skill_path: Path, mock: bool = False) -> dict:
"""Main entry: read SKILL.md, generate task_suite.yaml content.
Args:
skill_path: Path to the skill directory (containing SKILL.md).
mock: If True, skip any LLM-based calibration.
Returns:
A dict representing the task_suite.yaml content.
"""
skill_md_path = skill_path / "SKILL.md"
if not skill_md_path.exists():
raise FileNotFoundError(f"No SKILL.md found at {skill_md_path}")
skill_md = skill_md_path.read_text()
frontmatter, body = parse_frontmatter(skill_md)
tasks: list[GeneratedTask] = []
# Strategy 1: From description/triggers
tasks.extend(generate_trigger_tasks(frontmatter))
# Strategy 2: From "When to Use" section
tasks.extend(generate_when_to_use_tasks(body, frontmatter))
# Strategy 3: From <example> tags
tasks.extend(generate_example_tasks(body, frontmatter))
# Strategy 4: From <anti-example> tags
tasks.extend(generate_anti_example_tasks(body, frontmatter))
# Strategy 5: From output format/CLI sections
tasks.extend(generate_output_format_tasks(body, frontmatter))
# Deduplicate and limit
tasks = deduplicate_tasks(tasks)
tasks = tasks[:10] # Max 10 tasks per suite
return {
"skill_id": frontmatter.get("name", skill_path.name),
"version": "1.0",
"generated_by": "skill-forge",
"tasks": [asdict(t) for t in tasks],
}
def parse_frontmatter(content: str) -> tuple[dict, str]:
"""Split YAML frontmatter from markdown body.
Returns:
Tuple of (frontmatter_dict, body_string).
"""
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 3:
fm = yaml.safe_load(parts[1]) or {}
return fm, parts[2]
return {}, content
def generate_trigger_tasks(fm: dict) -> list[GeneratedTask]:
"""From description field, generate a task that tests the skill's core promise."""
desc = fm.get("description", "")
name = fm.get("name", "unknown")
if not desc:
return []
return [
GeneratedTask(
id=f"{name}-core-capability",
description="Test core capability described in skill description",
prompt=(
f"You are an AI assistant with this skill loaded: {name}. "
f"{desc}\n\n"
f"Demonstrate the primary capability described above "
f"with a concrete example."
),
judge={
"type": "llm-rubric",
"rubric": (
f"The output should demonstrate the capability described as: "
f"'{desc}'. Score 1.0 if the output clearly addresses the "
f"described use case, 0.0 if it's generic or unrelated."
),
"pass_threshold": 0.7,
},
source="frontmatter.description",
)
]
def generate_when_to_use_tasks(body: str, fm: dict) -> list[GeneratedTask]:
"""Extract 'When to Use' scenarios and create positive tests."""
name = fm.get("name", "unknown")
tasks: list[GeneratedTask] = []
# Find "When to Use" or equivalent Chinese/English section headers
when_match = re.search(
r"##\s*(?:When to Use|使用场景|使用|用法|核心流程)\s*\n(.*?)(?=\n##|\Z)",
body,
re.DOTALL,
)
if not when_match:
return []
when_text = when_match.group(1)
# Extract bullet points (-, *, or numbered)
bullets = re.findall(r"[-*]\s+(.+)", when_text)
if not bullets:
# Try numbered list
bullets = re.findall(r"\d+\.\s+(.+)", when_text)
for i, bullet in enumerate(bullets[:3]): # Max 3 from this source
# Clean up markdown formatting in bullet
clean_bullet = re.sub(r"[*_`]", "", bullet).strip()
tasks.append(
GeneratedTask(
id=f"{name}-use-case-{i + 1:02d}",
description=f"Use case: {clean_bullet[:80]}",
prompt=(
f"Scenario: {clean_bullet}\n\n"
f"Using the {name} skill, handle this scenario "
f"and produce the expected output."
),
judge={
"type": "llm-rubric",
"rubric": (
f"The output should address this use case: "
f"'{clean_bullet}'. Score based on relevance "
f"and completeness."
),
"pass_threshold": 0.6,
},
source="when_to_use",
)
)
return tasks
def generate_example_tasks(body: str, fm: dict) -> list[GeneratedTask]:
"""Extract <example> tags and create tests that verify similar output."""
name = fm.get("name", "unknown")
tasks: list[GeneratedTask] = []
examples = re.findall(r"<example>\s*(.*?)\s*</example>", body, re.DOTALL)
for i, example in enumerate(examples[:2]): # Max 2 from examples
# Extract key phrases from the example
lines = [
line.strip()
for line in example.split("\n")
if line.strip() and not line.strip().startswith("#")
]
if not lines:
continue
# Use first meaningful line as the scenario description
scenario = lines[0]
# Extract keywords that should appear in correct output
keywords = extract_keywords(example)
if keywords:
tasks.append(
GeneratedTask(
id=f"{name}-example-{i + 1:02d}",
description=f"Matches example pattern: {scenario[:60]}",
prompt=(
f"Following the {name} skill pattern shown in this "
f"example:\n{example}\n\n"
f"Produce similar output for a comparable scenario."
),
judge={"type": "contains", "expected": keywords[:4]},
source="example_tag",
)
)
return tasks
def generate_anti_example_tasks(body: str, fm: dict) -> list[GeneratedTask]:
"""Extract <anti-example> tags and create tests that verify the skill avoids bad patterns."""
name = fm.get("name", "unknown")
tasks: list[GeneratedTask] = []
anti_examples = re.findall(
r"<anti-example>\s*(.*?)\s*</anti-example>", body, re.DOTALL
)
for i, anti in enumerate(anti_examples[:2]):
tasks.append(
GeneratedTask(
id=f"{name}-anti-pattern-{i + 1:02d}",
description="Should avoid anti-pattern described in anti-example",
prompt=(
f"Using the {name} skill, handle a task correctly. "
f"The following is an INCORRECT approach that should "
f"be avoided:\n{anti}\n\n"
f"Provide the CORRECT approach instead."
),
judge={
"type": "llm-rubric",
"rubric": (
f"The output should NOT follow this anti-pattern: "
f"'{anti[:200]}'. Score 1.0 if the output correctly "
f"avoids the described mistake, 0.0 if it repeats "
f"the anti-pattern."
),
"pass_threshold": 0.7,
},
source="anti_example_tag",
)
)
return tasks
def generate_output_format_tasks(body: str, fm: dict) -> list[GeneratedTask]:
"""If the skill specifies output format/artifacts, test that output matches."""
name = fm.get("name", "unknown")
tasks: list[GeneratedTask] = []
# Look for "Output Artifacts" table or equivalent sections
output_match = re.search(
r"##\s*(?:Output|输出|Output Artifacts)\s*\n(.*?)(?=\n##|\Z)",
body,
re.DOTALL,
)
if not output_match:
return []
output_text = output_match.group(1)
# Extract deliverable types from table rows
deliverables = re.findall(r"\|\s*(.+?)\s*\|\s*(.+?)\s*\|", output_text)
found = 0
for request, deliverable in deliverables:
if found >= 1:
break
req = request.strip().strip("-")
deliv = deliverable.strip().strip("-")
# Skip header/separator rows
if not req or not deliv or "Request" in req or "---" in req:
continue
tasks.append(
GeneratedTask(
id=f"{name}-output-format",
description=f"Output should match: {deliv[:60]}",
prompt=(
f"Using the {name} skill for: {req}\n"
f"Produce the expected deliverable."
),
judge={
"type": "llm-rubric",
"rubric": (
f"The output should be: {deliv}. Score based on "
f"format compliance and completeness."
),
"pass_threshold": 0.6,
},
source="output_artifacts",
)
)
found += 1
return tasks
def extract_keywords(text: str) -> list[str]:
"""Extract meaningful keywords from example text for ContainsJudge.
Tries quoted strings first, then falls back to distinctive CamelCase
or long lowercase identifiers.
"""
# Remove code blocks, URLs, common markdown
clean = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
clean = re.sub(r"https?://\S+", "", clean)
clean = re.sub(r"[#*`\-\u2192]", "", clean)
# Find quoted strings first (most specific)
quoted = re.findall(r'["\']([^"\']+)["\']', clean)
if quoted:
return [q for q in quoted if len(q) > 2][:4]
# Fall back to distinctive words: CamelCase or long identifiers
words = re.findall(r"[A-Z][a-zA-Z]+(?:\.[a-zA-Z]+)*|[a-z_]{5,}", clean)
# Deduplicate while preserving order
seen: set[str] = set()
unique: list[str] = []
for w in words:
if w not in seen:
seen.add(w)
unique.append(w)
return unique[:4]
def deduplicate_tasks(tasks: list[GeneratedTask]) -> list[GeneratedTask]:
"""Remove tasks with duplicate IDs."""
seen_ids: set[str] = set()
result: list[GeneratedTask] = []
for t in tasks:
if t.id not in seen_ids:
seen_ids.add(t.id)
result.append(t)
return result
def write_task_suite(suite: dict, output_path: Path) -> Path:
"""Write a task suite dict to a YAML file.
Args:
suite: The task suite dict (from generate_task_suite).
output_path: Directory to write task_suite.yaml into.
Returns:
The path to the written file.
"""
output_path.mkdir(parents=True, exist_ok=True)
out_file = output_path / "task_suite.yaml"
with open(out_file, "w") as f:
yaml.dump(
suite,
f,
allow_unicode=True,
default_flow_style=False,
sort_keys=False,
)
return out_file
"""Tests for forge.py CLI — integration tests for both modes."""
import pytest
import sys
import yaml
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.forge import handle_from_skill, handle_from_spec
from interfaces.spec_schema import SkillSpec
class TestForgeFromSkill:
"""Test --from-skill mode (generate task suite for existing skill)."""
def test_generates_task_suite_yaml(self, tmp_path):
# Create a minimal skill
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"---\nname: my-skill\ndescription: does stuff\n---\n"
"# My Skill\n## When to Use\n- When doing stuff\n"
)
output_dir = tmp_path / "output"
output_dir.mkdir()
# Create a namespace mock for args
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_skill(skill_dir, output_dir, Args())
assert result == 0
# Check output file exists
suite_file = output_dir / "task_suite.yaml"
assert suite_file.exists()
# Check it's valid YAML
suite = yaml.safe_load(suite_file.read_text())
assert suite["skill_id"] == "my-skill"
assert len(suite["tasks"]) >= 1
def test_output_is_valid_yaml(self, tmp_path):
skill_dir = tmp_path / "yaml-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"---\nname: yaml-test\ndescription: tests yaml output\n---\n"
"## When to Use\n- Testing YAML validity\n"
)
output_dir = tmp_path / "out"
output_dir.mkdir()
class Args:
mock = True
evaluate = False
auto_improve = False
handle_from_skill(skill_dir, output_dir, Args())
suite_file = output_dir / "task_suite.yaml"
# Should not raise
suite = yaml.safe_load(suite_file.read_text())
assert isinstance(suite, dict)
assert "tasks" in suite
for task in suite["tasks"]:
assert isinstance(task, dict)
def test_fails_for_nonexistent_dir(self, tmp_path):
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_skill(
tmp_path / "nonexistent", tmp_path, Args()
)
assert result == 1
def test_fails_for_missing_skill_md(self, tmp_path):
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_skill(empty_dir, tmp_path, Args())
assert result == 1
class TestForgeFromSpec:
"""Test --from-spec mode (generate skill + task suite from spec)."""
def test_generates_skill_directory(self, tmp_path):
spec_file = tmp_path / "spec.yaml"
spec_file.write_text(
yaml.dump(
{
"name": "new-skill",
"purpose": "Test new skill generation",
"inputs": [
{"name": "data", "description": "input data"}
],
"outputs": [
{"name": "report", "format": "markdown"}
],
}
)
)
output_dir = tmp_path / "output"
output_dir.mkdir()
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_spec(spec_file, output_dir, Args())
assert result == 0
# Check skill directory was created
skill_dir = output_dir / "new-skill"
assert skill_dir.is_dir()
assert (skill_dir / "SKILL.md").exists()
assert (skill_dir / "task_suite.yaml").exists()
def test_generated_skill_has_frontmatter(self, tmp_path):
spec_file = tmp_path / "spec.yaml"
spec_file.write_text(
yaml.dump(
{
"name": "fm-test",
"purpose": "Test frontmatter generation",
}
)
)
output_dir = tmp_path / "output"
output_dir.mkdir()
class Args:
mock = True
evaluate = False
auto_improve = False
handle_from_spec(spec_file, output_dir, Args())
skill_md = (output_dir / "fm-test" / "SKILL.md").read_text()
assert skill_md.startswith("---")
assert "name: fm-test" in skill_md
def test_fails_for_invalid_spec(self, tmp_path):
spec_file = tmp_path / "bad.yaml"
spec_file.write_text(yaml.dump({"name": "", "purpose": ""}))
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_spec(spec_file, tmp_path, Args())
assert result == 1
def test_fails_for_missing_spec(self, tmp_path):
class Args:
mock = True
evaluate = False
auto_improve = False
result = handle_from_spec(
tmp_path / "nonexistent.yaml", tmp_path, Args()
)
assert result == 1
class TestSpecSchema:
"""Test SkillSpec validation."""
def test_valid_spec(self, tmp_path):
spec_file = tmp_path / "valid.yaml"
spec_file.write_text(
yaml.dump({"name": "test", "purpose": "Test skill"})
)
spec = SkillSpec.from_yaml(spec_file)
assert spec.validate() == []
def test_missing_name(self, tmp_path):
spec_file = tmp_path / "no_name.yaml"
spec_file.write_text(yaml.dump({"name": "", "purpose": "Test"}))
spec = SkillSpec.from_yaml(spec_file)
errors = spec.validate()
assert any("name" in e for e in errors)
def test_missing_purpose(self, tmp_path):
spec_file = tmp_path / "no_purpose.yaml"
spec_file.write_text(yaml.dump({"name": "test", "purpose": ""}))
spec = SkillSpec.from_yaml(spec_file)
errors = spec.validate()
assert any("purpose" in e for e in errors)
def test_name_too_long(self, tmp_path):
spec_file = tmp_path / "long_name.yaml"
spec_file.write_text(
yaml.dump({"name": "a" * 51, "purpose": "Test"})
)
spec = SkillSpec.from_yaml(spec_file)
errors = spec.validate()
assert any("too long" in e for e in errors)
def test_loads_all_fields(self, tmp_path):
spec_file = tmp_path / "full.yaml"
spec_file.write_text(
yaml.dump(
{
"name": "full-test",
"purpose": "Full test",
"inputs": [{"name": "a"}],
"outputs": [{"name": "b"}],
"quality_criteria": [{"name": "c"}],
"domain_knowledge": ["fact1"],
"reference_skills": ["skill1"],
}
)
)
spec = SkillSpec.from_yaml(spec_file)
assert spec.name == "full-test"
assert len(spec.inputs) == 1
assert len(spec.outputs) == 1
assert len(spec.quality_criteria) == 1
assert spec.domain_knowledge == ["fact1"]
assert spec.reference_skills == ["skill1"]
"""Tests for skill_generator — SKILL.md generation from spec."""
import pytest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.skill_generator import (
generate_skill_from_spec,
derive_triggers,
derive_description,
)
class TestGenerateSkillFromSpec:
def test_basic_generation(self):
spec = {
"name": "test-skill",
"purpose": "Test things",
"inputs": [{"name": "code", "description": "source code to test"}],
"outputs": [{"name": "report", "format": "markdown"}],
}
result = generate_skill_from_spec(spec)
assert "---" in result
assert "name: test-skill" in result
assert "When to Use" in result
assert "When NOT to Use" in result
assert "Output Artifacts" in result
def test_includes_domain_knowledge(self):
spec = {
"name": "test-skill",
"purpose": "Test things",
"domain_knowledge": ["Fact one", "Fact two"],
}
result = generate_skill_from_spec(spec)
assert "Domain Knowledge" in result
assert "Fact one" in result
assert "Fact two" in result
def test_includes_quality_criteria_examples(self):
spec = {
"name": "test-skill",
"purpose": "Test things",
"quality_criteria": [
{"name": "accuracy", "description": "Must be accurate"},
],
}
result = generate_skill_from_spec(spec)
assert "<example>" in result
assert "<anti-example>" in result
assert "accuracy" in result.lower() or "accurate" in result.lower()
def test_includes_reference_skills(self):
spec = {
"name": "test-skill",
"purpose": "Test things",
"reference_skills": ["other-skill", "another-skill"],
}
result = generate_skill_from_spec(spec)
assert "Related Skills" in result
assert "other-skill" in result
def test_minimal_spec(self):
spec = {"name": "minimal", "purpose": "Do minimal things"}
result = generate_skill_from_spec(spec)
assert "---" in result
assert "name: minimal" in result
assert "Do minimal things" in result
def test_title_formatting(self):
spec = {"name": "my-cool-skill", "purpose": "Be cool"}
result = generate_skill_from_spec(spec)
assert "# My Cool Skill" in result
def test_empty_inputs_fallback(self):
spec = {"name": "test", "purpose": "Do testing work"}
result = generate_skill_from_spec(spec)
assert "When to Use" in result
# Should use purpose-based fallback
assert "do testing work" in result.lower()
class TestDeriveTriggers:
def test_includes_name(self):
triggers = derive_triggers("my-skill", "analyze code quality")
assert "my-skill" in triggers
def test_includes_name_without_hyphens(self):
triggers = derive_triggers("my-skill", "analyze code quality")
assert "my skill" in triggers
def test_filters_short_words(self):
triggers = derive_triggers("test", "do it now")
# "do", "it", "now" are all <= 3 chars
assert len(triggers) <= 2 # just name variants
def test_max_six_triggers(self):
triggers = derive_triggers(
"test",
"word1234 word2345 word3456 word4567 word5678 word6789 word7890",
)
assert len(triggers) <= 6
def test_deduplicates(self):
triggers = derive_triggers("analyze", "Analyze things carefully")
# "analyze" appears in both name and purpose
assert triggers.count("analyze") <= 1
def test_filters_stop_words(self):
triggers = derive_triggers("test", "this that with from into")
# All purpose words are stop words
assert len(triggers) <= 2 # just name + "test"
class TestDeriveDescription:
def test_with_inputs(self):
desc = derive_description(
"Analyze code",
[{"description": "source code"}, {"description": "config file"}],
)
assert "Analyze code" in desc
assert "source code" in desc
def test_without_inputs(self):
desc = derive_description("Analyze code", [])
assert desc == "Analyze code"
def test_truncates_long_input_descriptions(self):
desc = derive_description(
"Test",
[{"description": "a" * 100}],
)
# Should truncate at 30 chars
assert len(desc) < 150
"""Tests for task_suite_generator — the core module of skill-forge."""
import pytest
import sys
from pathlib import Path
# Add skill root to path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.task_suite_generator import (
parse_frontmatter,
generate_task_suite,
generate_trigger_tasks,
generate_when_to_use_tasks,
generate_example_tasks,
generate_anti_example_tasks,
generate_output_format_tasks,
extract_keywords,
deduplicate_tasks,
GeneratedTask,
)
class TestParseFrontmatter:
def test_valid_frontmatter(self):
content = "---\nname: test\ndescription: a test\n---\n\n# Body"
fm, body = parse_frontmatter(content)
assert fm["name"] == "test"
assert fm["description"] == "a test"
assert "Body" in body
def test_no_frontmatter(self):
fm, body = parse_frontmatter("# Just body")
assert fm == {}
assert "Just body" in body
def test_empty_frontmatter(self):
content = "---\n---\n\n# Body"
fm, body = parse_frontmatter(content)
assert fm == {}
assert "Body" in body
def test_frontmatter_with_list(self):
content = "---\nname: test\ntriggers:\n - foo\n - bar\n---\n\nbody"
fm, body = parse_frontmatter(content)
assert fm["triggers"] == ["foo", "bar"]
def test_content_without_triple_dashes(self):
content = "Just some content without frontmatter"
fm, body = parse_frontmatter(content)
assert fm == {}
assert body == content
class TestGenerateTriggerTasks:
def test_generates_from_description(self):
fm = {"name": "test-skill", "description": "Analyze code for bugs"}
tasks = generate_trigger_tasks(fm)
assert len(tasks) == 1
assert tasks[0].id == "test-skill-core-capability"
assert tasks[0].judge["type"] == "llm-rubric"
assert "Analyze code for bugs" in tasks[0].judge["rubric"]
def test_empty_description(self):
tasks = generate_trigger_tasks({"name": "test", "description": ""})
assert len(tasks) == 0
def test_missing_description(self):
tasks = generate_trigger_tasks({"name": "test"})
assert len(tasks) == 0
def test_missing_name_uses_unknown(self):
tasks = generate_trigger_tasks({"description": "Do stuff"})
assert len(tasks) == 1
assert tasks[0].id == "unknown-core-capability"
class TestGenerateWhenToUseTasks:
def test_extracts_bullet_points(self):
body = "\n## When to Use\n- When running tests\n- When validating output\n- When checking format\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 3
assert tasks[0].id == "test-use-case-01"
assert tasks[1].id == "test-use-case-02"
assert tasks[2].id == "test-use-case-03"
def test_max_three_bullets(self):
body = "\n## When to Use\n- A\n- B\n- C\n- D\n- E\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 3
def test_no_when_section(self):
body = "\n## Other Section\n- stuff\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 0
def test_chinese_header(self):
body = "\n## 使用场景\n- 当需要运行测试时\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 1
def test_asterisk_bullets(self):
body = "\n## When to Use\n* First scenario\n* Second scenario\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 2
def test_section_boundary(self):
body = "\n## When to Use\n- Valid task\n\n## When NOT to Use\n- Invalid task\n"
tasks = generate_when_to_use_tasks(body, {"name": "test"})
assert len(tasks) == 1
assert "Valid task" in tasks[0].prompt
class TestGenerateExampleTasks:
def test_extracts_from_example_tags(self):
body = """
## Usage
<example>
Correct: run `generate_notes --platform ios`
produces structured markdown with commit refs
</example>
"""
tasks = generate_example_tasks(body, {"name": "test"})
assert len(tasks) >= 1
assert tasks[0].source == "example_tag"
assert tasks[0].judge["type"] == "contains"
def test_multiple_examples(self):
body = """
<example>
First example with "keyword1"
</example>
<example>
Second example with "keyword2"
</example>
<example>
Third example with "keyword3"
</example>
"""
tasks = generate_example_tasks(body, {"name": "test"})
# Max 2 from examples
assert len(tasks) <= 2
def test_empty_example(self):
body = "<example>\n\n</example>"
tasks = generate_example_tasks(body, {"name": "test"})
assert len(tasks) == 0
def test_example_with_only_headers(self):
body = "<example>\n# Just a header\n</example>"
tasks = generate_example_tasks(body, {"name": "test"})
# Headers are filtered out, so no lines remain
assert len(tasks) == 0
class TestGenerateAntiExampleTasks:
def test_extracts_from_anti_example_tags(self):
body = """
<anti-example>
Wrong: using orchestrator just to check scores
should use improvement-learner instead
</anti-example>
"""
tasks = generate_anti_example_tasks(body, {"name": "test"})
assert len(tasks) == 1
assert "anti-pattern" in tasks[0].id
assert tasks[0].judge["type"] == "llm-rubric"
def test_max_two_anti_examples(self):
body = """
<anti-example>Anti 1</anti-example>
<anti-example>Anti 2</anti-example>
<anti-example>Anti 3</anti-example>
"""
tasks = generate_anti_example_tasks(body, {"name": "test"})
assert len(tasks) == 2
class TestGenerateOutputFormatTasks:
def test_extracts_from_output_table(self):
body = (
"\n## Output Artifacts\n"
"| Request | Deliverable |\n"
"|---------|------------|\n"
"| Test run | JSON report with pass/fail counts |\n"
)
tasks = generate_output_format_tasks(body, {"name": "test"})
assert len(tasks) == 1
assert tasks[0].source == "output_artifacts"
def test_skips_header_row(self):
body = """
## Output Artifacts
| Request | Deliverable |
|---------|------------|
"""
tasks = generate_output_format_tasks(body, {"name": "test"})
assert len(tasks) == 0
def test_no_output_section(self):
body = "## Other Section\nstuff"
tasks = generate_output_format_tasks(body, {"name": "test"})
assert len(tasks) == 0
class TestExtractKeywords:
def test_extracts_quoted_strings(self):
kw = extract_keywords('Use "ContainsJudge" for "keyword matching"')
assert "ContainsJudge" in kw
assert "keyword matching" in kw
def test_extracts_camelcase(self):
kw = extract_keywords("Use TaskRunner and PytestJudge")
assert "TaskRunner" in kw
assert "PytestJudge" in kw
def test_filters_short_quoted(self):
kw = extract_keywords('"a" and "be" and "long enough"')
assert "a" not in kw
assert "be" not in kw
assert "long enough" in kw
def test_deduplicates(self):
kw = extract_keywords("TaskRunner TaskRunner TaskRunner")
assert kw.count("TaskRunner") == 1
def test_max_four_keywords(self):
kw = extract_keywords('"a1234" "b1234" "c1234" "d1234" "e1234"')
assert len(kw) <= 4
def test_code_blocks_removed(self):
kw = extract_keywords("```python\ncode_here\n```\nSomeClass outside")
assert "SomeClass" in kw
class TestDeduplication:
def test_removes_duplicate_ids(self):
t1 = GeneratedTask(id="a", description="x", prompt="p", judge={})
t2 = GeneratedTask(id="a", description="y", prompt="q", judge={})
t3 = GeneratedTask(id="b", description="z", prompt="r", judge={})
result = deduplicate_tasks([t1, t2, t3])
assert len(result) == 2
# First occurrence wins
assert result[0].description == "x"
def test_preserves_order(self):
tasks = [
GeneratedTask(id="c", description="3", prompt="p", judge={}),
GeneratedTask(id="a", description="1", prompt="p", judge={}),
GeneratedTask(id="b", description="2", prompt="p", judge={}),
]
result = deduplicate_tasks(tasks)
assert [t.id for t in result] == ["c", "a", "b"]
def test_empty_list(self):
assert deduplicate_tasks([]) == []
class TestFullGeneration:
def test_generates_from_real_skill(self, tmp_path):
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
name: test-skill
description: Test skill for unit testing
triggers:
- test
---
# Test Skill
## When to Use
- When you need to run tests
- When validating output format
<example>
Run: python3 -m pytest tests/ -v
All tests pass
</example>
<anti-example>
Wrong: skipping tests before commit
</anti-example>
## Output Artifacts
| Request | Deliverable |
|---------|------------|
| Test run | JSON report with pass/fail counts |
"""
)
suite = generate_task_suite(skill_dir)
assert suite["skill_id"] == "test-skill"
assert suite["version"] == "1.0"
assert suite["generated_by"] == "skill-forge"
assert len(suite["tasks"]) >= 3 # trigger + use-cases + example/anti
# Verify task structure
for task in suite["tasks"]:
assert "id" in task
assert "prompt" in task
assert "judge" in task
assert "description" in task
def test_max_ten_tasks(self, tmp_path):
"""Even with many sections, output is capped at 10 tasks."""
skill_dir = tmp_path / "big-skill"
skill_dir.mkdir()
bullets = "\n".join(f"- Use case {i}" for i in range(20))
examples = "\n".join(
f'<example>\nExample {i} with "keyword{i}"\n</example>'
for i in range(10)
)
(skill_dir / "SKILL.md").write_text(
f"---\nname: big\ndescription: Big skill\n---\n\n"
f"## When to Use\n{bullets}\n\n{examples}\n"
)
suite = generate_task_suite(skill_dir)
assert len(suite["tasks"]) <= 10
def test_missing_skill_md(self, tmp_path):
skill_dir = tmp_path / "empty-skill"
skill_dir.mkdir()
with pytest.raises(FileNotFoundError):
generate_task_suite(skill_dir)
def test_minimal_skill(self, tmp_path):
"""A SKILL.md with only frontmatter still produces at least 1 task."""
skill_dir = tmp_path / "minimal"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"---\nname: minimal\ndescription: Does something\n---\n"
)
suite = generate_task_suite(skill_dir)
assert len(suite["tasks"]) >= 1
assert suite["tasks"][0]["id"] == "minimal-core-capability"
def test_no_duplicate_ids_in_output(self, tmp_path):
skill_dir = tmp_path / "dedup-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
"""---
name: dedup
description: Test deduplication
---
## When to Use
- First scenario
- Second scenario
<example>
Example with "keyword"
</example>
"""
)
suite = generate_task_suite(skill_dir)
ids = [t["id"] for t in suite["tasks"]]
assert len(ids) == len(set(ids)), "Duplicate task IDs found"