neo-skill 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.shared/skill-creator/data/domains.json +56 -0
- package/.shared/skill-creator/data/output-patterns.csv +8 -0
- package/.shared/skill-creator/data/resource-patterns.csv +8 -0
- package/.shared/skill-creator/data/skill-reasoning.csv +11 -0
- package/.shared/skill-creator/data/trigger-patterns.csv +8 -0
- package/.shared/skill-creator/data/validation-rules.csv +11 -0
- package/.shared/skill-creator/data/workflow-patterns.csv +6 -0
- package/.shared/skill-creator/scripts/generate.py +300 -0
- package/.shared/skill-creator/scripts/package.py +140 -0
- package/.shared/skill-creator/scripts/search.py +231 -0
- package/.shared/skill-creator/scripts/validate.py +213 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/omni-skill.js +55 -0
- package/bin/skill-creator.js +55 -0
- package/package.json +25 -0
- package/skills/review-gate/references/review-gate.md +228 -0
- package/skills/review-gate/skillspec.json +131 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/pre-delivery-checklist.md +70 -0
- package/skills/skill-creator/references/requirement-collection.md +80 -0
- package/skills/skill-creator/references/skill-system-design.md +112 -0
- package/skills/skill-creator/references/sources.md +5 -0
- package/skills/skill-creator/references/workflow-step-editing.md +103 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +34 -0
- package/skills/skill-creator/scripts/package_skill.py +34 -0
- package/skills/skill-creator/scripts/validate_skill.py +35 -0
- package/skills/skill-creator/skillspec.json +117 -0
- package/src/omni_skill/__init__.py +1 -0
- package/src/omni_skill/cli.py +270 -0
- package/src/skill_creator/__init__.py +1 -0
- package/src/skill_creator/cli.py +278 -0
- package/src/skill_creator/packaging/package.py +30 -0
- package/src/skill_creator/packaging/ziputil.py +26 -0
- package/src/skill_creator/spec/model.py +111 -0
- package/src/skill_creator/spec/render.py +108 -0
- package/src/skill_creator/spec/validate.py +18 -0
- package/src/skill_creator/targets/claude.py +53 -0
- package/src/skill_creator/targets/common.py +46 -0
- package/src/skill_creator/targets/cursor.py +34 -0
- package/src/skill_creator/targets/github_skills.py +40 -0
- package/src/skill_creator/targets/windsurf.py +123 -0
- package/src/skill_creator/util/frontmatter.py +24 -0
- package/src/skill_creator/util/fs.py +32 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
from ..util.fs import read_json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _as_list(x: Any) -> List[Any]:
|
|
11
|
+
if x is None:
|
|
12
|
+
return []
|
|
13
|
+
if isinstance(x, list):
|
|
14
|
+
return x
|
|
15
|
+
return [x]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _kebab_case_ok(name: str) -> bool:
|
|
19
|
+
return (
|
|
20
|
+
bool(name)
|
|
21
|
+
and all(c.islower() or c.isdigit() or c == "-" for c in name)
|
|
22
|
+
and "--" not in name
|
|
23
|
+
and not name.startswith("-")
|
|
24
|
+
and not name.endswith("-")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class WorkflowStep:
|
|
30
|
+
id: str
|
|
31
|
+
title: str
|
|
32
|
+
kind: str = "action" # action|gate|branch
|
|
33
|
+
commands: List[str] = field(default_factory=list)
|
|
34
|
+
notes: str = ""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def from_dict(d: Dict[str, Any]) -> "WorkflowStep":
|
|
38
|
+
return WorkflowStep(
|
|
39
|
+
id=str(d.get("id", "")).strip(),
|
|
40
|
+
title=str(d.get("title", "")).strip(),
|
|
41
|
+
kind=str(d.get("kind", "action")).strip(),
|
|
42
|
+
commands=[str(c) for c in _as_list(d.get("commands"))],
|
|
43
|
+
notes=str(d.get("notes", "")).strip(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SkillSpec:
|
|
49
|
+
version: int
|
|
50
|
+
name: str
|
|
51
|
+
description: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Prompting
|
|
55
|
+
questions: List[str] = field(default_factory=list) # must be <= 10
|
|
56
|
+
triggers: List[str] = field(default_factory=list)
|
|
57
|
+
freedom_level: str = "low" # low|medium|high
|
|
58
|
+
|
|
59
|
+
# Workflow + assets
|
|
60
|
+
workflow_type: str = "sequential" # sequential|conditional
|
|
61
|
+
steps: List[WorkflowStep] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
references: List[str] = field(default_factory=list)
|
|
64
|
+
scripts: List[str] = field(default_factory=list)
|
|
65
|
+
assets: List[str] = field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def from_path(path: Path) -> "SkillSpec":
|
|
69
|
+
data = read_json(path)
|
|
70
|
+
return SkillSpec.from_dict(data)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_dict(d: Dict[str, Any]) -> "SkillSpec":
|
|
74
|
+
steps = [WorkflowStep.from_dict(x) for x in _as_list(d.get("workflow", {}).get("steps"))]
|
|
75
|
+
return SkillSpec(
|
|
76
|
+
version=int(d.get("version", 1)),
|
|
77
|
+
name=str(d.get("name", "")).strip(),
|
|
78
|
+
description=str(d.get("description", "")).strip(),
|
|
79
|
+
questions=[str(x) for x in _as_list(d.get("questions"))],
|
|
80
|
+
triggers=[str(x) for x in _as_list(d.get("triggers"))],
|
|
81
|
+
freedom_level=str(d.get("freedom_level", "low")).strip(),
|
|
82
|
+
workflow_type=str(d.get("workflow", {}).get("type", "sequential")).strip(),
|
|
83
|
+
steps=steps,
|
|
84
|
+
references=[str(x) for x in _as_list(d.get("references"))],
|
|
85
|
+
scripts=[str(x) for x in _as_list(d.get("scripts"))],
|
|
86
|
+
assets=[str(x) for x in _as_list(d.get("assets"))],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def validate_basic(self) -> List[str]:
|
|
90
|
+
errors: List[str] = []
|
|
91
|
+
if self.version != 1:
|
|
92
|
+
errors.append(f"Unsupported spec version: {self.version}")
|
|
93
|
+
if not _kebab_case_ok(self.name):
|
|
94
|
+
errors.append("name must be kebab-case (lowercase letters, digits, hyphen)")
|
|
95
|
+
if not self.description:
|
|
96
|
+
errors.append("description is required")
|
|
97
|
+
if len(self.questions) > 10:
|
|
98
|
+
errors.append("questions must be <= 10")
|
|
99
|
+
if self.freedom_level not in {"low", "medium", "high"}:
|
|
100
|
+
errors.append("freedom_level must be one of: low, medium, high")
|
|
101
|
+
if self.workflow_type not in {"sequential", "conditional"}:
|
|
102
|
+
errors.append("workflow.type must be sequential or conditional")
|
|
103
|
+
if not self.steps:
|
|
104
|
+
errors.append("workflow.steps must not be empty")
|
|
105
|
+
# minimal step validation
|
|
106
|
+
for i, s in enumerate(self.steps):
|
|
107
|
+
if not s.id:
|
|
108
|
+
errors.append(f"workflow.steps[{i}].id is required")
|
|
109
|
+
if not s.title:
|
|
110
|
+
errors.append(f"workflow.steps[{i}].title is required")
|
|
111
|
+
return errors
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from ..util.fs import ensure_dir, copytree, copyfile
|
|
7
|
+
from .model import SkillSpec
|
|
8
|
+
from ..targets.claude import render_claude_skill_md
|
|
9
|
+
from ..targets.windsurf import render_windsurf_workflow_md
|
|
10
|
+
from ..targets.cursor import render_cursor_command_md
|
|
11
|
+
from ..targets.github_skills import render_github_skill_md
|
|
12
|
+
|
|
13
|
+
# Primary target is Windsurf; others are backward compat
|
|
14
|
+
PRIMARY_TARGET = "windsurf"
|
|
15
|
+
BACKWARD_COMPAT_TARGETS = ["claude", "cursor", "github"]
|
|
16
|
+
ALL_TARGETS = [PRIMARY_TARGET] + BACKWARD_COMPAT_TARGETS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_target(repo_root: Path, spec: SkillSpec, spec_dir: Path, target: str) -> Path:
|
|
20
|
+
"""Generate output for a specific target. Returns output path."""
|
|
21
|
+
refs_dir = spec_dir / "references"
|
|
22
|
+
scripts_dir = spec_dir / "scripts"
|
|
23
|
+
assets_dir = spec_dir / "assets"
|
|
24
|
+
skill_readme = spec_dir / "readme.txt"
|
|
25
|
+
|
|
26
|
+
if target == "windsurf":
|
|
27
|
+
# PRIMARY: Windsurf workflow
|
|
28
|
+
out_path = repo_root / ".windsurf" / "workflows" / f"{spec.name}.md"
|
|
29
|
+
ensure_dir(out_path.parent)
|
|
30
|
+
out_path.write_text(render_windsurf_workflow_md(spec), encoding="utf-8")
|
|
31
|
+
|
|
32
|
+
# Windsurf workflow data (optional)
|
|
33
|
+
windsurf_data_src = assets_dir / "windsurf-workflow-data"
|
|
34
|
+
if windsurf_data_src.exists():
|
|
35
|
+
windsurf_data_dst = repo_root / ".windsurf" / "workflows" / "data" / spec.name
|
|
36
|
+
copytree(windsurf_data_src, windsurf_data_dst)
|
|
37
|
+
return out_path
|
|
38
|
+
|
|
39
|
+
elif target == "claude":
|
|
40
|
+
# BACKWARD COMPAT: Claude SKILL.md
|
|
41
|
+
claude_dir = repo_root / ".claude" / "skills" / spec.name
|
|
42
|
+
ensure_dir(claude_dir)
|
|
43
|
+
out_path = claude_dir / "SKILL.md"
|
|
44
|
+
out_path.write_text(render_claude_skill_md(spec), encoding="utf-8")
|
|
45
|
+
if skill_readme.exists():
|
|
46
|
+
copyfile(skill_readme, claude_dir / "readme.txt")
|
|
47
|
+
claude_res = claude_dir / "resources"
|
|
48
|
+
copytree(refs_dir, claude_res / "references")
|
|
49
|
+
copytree(scripts_dir, claude_res / "scripts")
|
|
50
|
+
copytree(assets_dir, claude_res / "assets")
|
|
51
|
+
return out_path
|
|
52
|
+
|
|
53
|
+
elif target == "cursor":
|
|
54
|
+
# BACKWARD COMPAT: Cursor command
|
|
55
|
+
out_path = repo_root / ".cursor" / "commands" / f"{spec.name}.md"
|
|
56
|
+
ensure_dir(out_path.parent)
|
|
57
|
+
out_path.write_text(render_cursor_command_md(spec), encoding="utf-8")
|
|
58
|
+
return out_path
|
|
59
|
+
|
|
60
|
+
elif target == "github":
|
|
61
|
+
# BACKWARD COMPAT: GitHub Skills
|
|
62
|
+
gh_dir = repo_root / ".github" / "skills" / spec.name
|
|
63
|
+
ensure_dir(gh_dir)
|
|
64
|
+
out_path = gh_dir / "SKILL.md"
|
|
65
|
+
out_path.write_text(render_github_skill_md(spec), encoding="utf-8")
|
|
66
|
+
if skill_readme.exists():
|
|
67
|
+
copyfile(skill_readme, gh_dir / "readme.txt")
|
|
68
|
+
copytree(refs_dir, gh_dir / "references")
|
|
69
|
+
copytree(scripts_dir, gh_dir / "scripts")
|
|
70
|
+
copytree(assets_dir, gh_dir / "assets")
|
|
71
|
+
return out_path
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(f"Unknown target: {target}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_windsurf(repo_root: Path, spec_path: Path) -> SkillSpec:
|
|
78
|
+
"""Generate Windsurf output only (primary target)."""
|
|
79
|
+
spec = SkillSpec.from_path(spec_path)
|
|
80
|
+
spec_dir = spec_path.parent
|
|
81
|
+
generate_target(repo_root, spec, spec_dir, "windsurf")
|
|
82
|
+
return spec
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_all(repo_root: Path, spec_path: Path, targets: Optional[List[str]] = None) -> SkillSpec:
|
|
86
|
+
"""
|
|
87
|
+
Generate outputs for specified targets.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
repo_root: Repository root path
|
|
91
|
+
spec_path: Path to skillspec.json
|
|
92
|
+
targets: List of targets to generate. Default: all targets.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Loaded SkillSpec
|
|
96
|
+
"""
|
|
97
|
+
repo_root = repo_root.resolve()
|
|
98
|
+
spec_path = spec_path.resolve()
|
|
99
|
+
spec = SkillSpec.from_path(spec_path)
|
|
100
|
+
spec_dir = spec_path.parent
|
|
101
|
+
|
|
102
|
+
if targets is None:
|
|
103
|
+
targets = ALL_TARGETS
|
|
104
|
+
|
|
105
|
+
for target in targets:
|
|
106
|
+
generate_target(repo_root, spec, spec_dir, target)
|
|
107
|
+
|
|
108
|
+
return spec
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from .model import SkillSpec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_spec(spec_path: Path) -> List[str]:
|
|
10
|
+
spec = SkillSpec.from_path(spec_path)
|
|
11
|
+
return spec.validate_basic()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assert_spec_valid(spec_path: Path) -> None:
|
|
15
|
+
errors = validate_spec(spec_path)
|
|
16
|
+
if errors:
|
|
17
|
+
joined = "\n- " + "\n- ".join(errors)
|
|
18
|
+
raise SystemExit(f"Spec validation failed:{joined}\n")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..spec.model import SkillSpec
|
|
4
|
+
from ..util.frontmatter import dump_frontmatter
|
|
5
|
+
from .common import _render_steps, _render_resources, _render_footer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_claude_skill_md(spec: SkillSpec) -> str:
|
|
9
|
+
# Strict metadata: only name + description.
|
|
10
|
+
fm = dump_frontmatter({"name": spec.name, "description": spec.description})
|
|
11
|
+
|
|
12
|
+
body = []
|
|
13
|
+
body.append("# " + spec.name)
|
|
14
|
+
body.append("")
|
|
15
|
+
body.append("## What this skill does")
|
|
16
|
+
body.append(spec.description)
|
|
17
|
+
body.append("")
|
|
18
|
+
|
|
19
|
+
if spec.triggers:
|
|
20
|
+
body.append("## When to use (examples of user requests)")
|
|
21
|
+
for t in spec.triggers:
|
|
22
|
+
body.append(f"- {t}")
|
|
23
|
+
body.append("")
|
|
24
|
+
|
|
25
|
+
body.append("## Degrees of freedom")
|
|
26
|
+
body.append(
|
|
27
|
+
f"This skill is designed for **{spec.freedom_level}** freedom (high=heuristic, low=deterministic scripts)."
|
|
28
|
+
)
|
|
29
|
+
body.append("")
|
|
30
|
+
|
|
31
|
+
if spec.questions:
|
|
32
|
+
body.append("## Requirement collection (at most 10 questions)")
|
|
33
|
+
body.append("Ask only what is missing. Never exceed 10 questions total.")
|
|
34
|
+
for i, q in enumerate(spec.questions, start=1):
|
|
35
|
+
body.append(f"{i}. {q}")
|
|
36
|
+
body.append("")
|
|
37
|
+
|
|
38
|
+
body.append("## Workflow")
|
|
39
|
+
body.append(f"*Mode:* `{spec.workflow_type}`")
|
|
40
|
+
body.append("")
|
|
41
|
+
body.append(_render_steps(spec.steps))
|
|
42
|
+
body.append("")
|
|
43
|
+
|
|
44
|
+
body.append(_render_resources(spec))
|
|
45
|
+
body.append("## Output contract")
|
|
46
|
+
body.append("When invoked, produce:")
|
|
47
|
+
body.append("A) A proposed directory tree")
|
|
48
|
+
body.append("B) SKILL.md / workflow entry file(s)")
|
|
49
|
+
body.append("C) Any scripts/references/assets")
|
|
50
|
+
body.append("D) A validation checklist with pass/fail")
|
|
51
|
+
|
|
52
|
+
body.append(_render_footer())
|
|
53
|
+
return fm + "\n".join(body)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from ..spec.model import SkillSpec, WorkflowStep
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _render_steps(steps: List[WorkflowStep]) -> str:
|
|
10
|
+
out = []
|
|
11
|
+
for i, s in enumerate(steps, start=1):
|
|
12
|
+
out.append(f"### Step {i} — {s.title}")
|
|
13
|
+
if s.kind:
|
|
14
|
+
out.append(f"*Type:* `{s.kind}`")
|
|
15
|
+
if s.notes:
|
|
16
|
+
out.append(s.notes)
|
|
17
|
+
if s.commands:
|
|
18
|
+
out.append("\n```bash")
|
|
19
|
+
out.extend(s.commands)
|
|
20
|
+
out.append("```\n")
|
|
21
|
+
return "\n".join(out)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _render_resources(spec: SkillSpec) -> str:
|
|
25
|
+
lines = []
|
|
26
|
+
if spec.references:
|
|
27
|
+
lines.append("## References (loaded on-demand)")
|
|
28
|
+
for p in spec.references:
|
|
29
|
+
lines.append(f"- `{p}`")
|
|
30
|
+
lines.append("")
|
|
31
|
+
if spec.scripts:
|
|
32
|
+
lines.append("## Scripts (deterministic tools)")
|
|
33
|
+
for p in spec.scripts:
|
|
34
|
+
lines.append(f"- `{p}`")
|
|
35
|
+
lines.append("")
|
|
36
|
+
if spec.assets:
|
|
37
|
+
lines.append("## Assets")
|
|
38
|
+
for p in spec.assets:
|
|
39
|
+
lines.append(f"- `{p}`")
|
|
40
|
+
lines.append("")
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _render_footer() -> str:
|
|
45
|
+
ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
46
|
+
return f"\n---\nGenerated by neo-skill at {ts}.\n"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..spec.model import SkillSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_cursor_command_md(spec: SkillSpec) -> str:
|
|
7
|
+
# Cursor's command palette prompts are typically Markdown instructions.
|
|
8
|
+
lines = [f"# {spec.name}", "", spec.description, ""]
|
|
9
|
+
if spec.triggers:
|
|
10
|
+
lines.append("## When to use")
|
|
11
|
+
lines.extend([f"- {t}" for t in spec.triggers])
|
|
12
|
+
lines.append("")
|
|
13
|
+
lines.append("## Command")
|
|
14
|
+
lines.append("Run these deterministic steps in the repository:")
|
|
15
|
+
lines.append("```bash")
|
|
16
|
+
if spec.name == "skill-creator":
|
|
17
|
+
lines.append("python3 .shared/skill-creator/scripts/generate.py skills/<skill>/skillspec.json")
|
|
18
|
+
lines.append("python3 .shared/skill-creator/scripts/validate.py skills/<skill>/skillspec.json")
|
|
19
|
+
lines.append("python3 .shared/skill-creator/scripts/package.py --target claude --skill <skill>")
|
|
20
|
+
else:
|
|
21
|
+
lines.append("omni-skill init --cursor")
|
|
22
|
+
lines.append("omni-skill do --agent")
|
|
23
|
+
lines.append("```")
|
|
24
|
+
lines.append("")
|
|
25
|
+
lines.append("## Workflow")
|
|
26
|
+
lines.append(f"Mode: `{spec.workflow_type}`")
|
|
27
|
+
lines.append("")
|
|
28
|
+
for i, step in enumerate(spec.steps, start=1):
|
|
29
|
+
lines.append(f"### {i}. {step.title}")
|
|
30
|
+
if step.commands:
|
|
31
|
+
lines.append("```bash")
|
|
32
|
+
lines.extend(step.commands)
|
|
33
|
+
lines.append("```")
|
|
34
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..spec.model import SkillSpec
|
|
4
|
+
from ..util.frontmatter import dump_frontmatter
|
|
5
|
+
from .common import _render_steps, _render_resources, _render_footer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_github_skill_md(spec: SkillSpec) -> str:
|
|
9
|
+
# GitHub/Agent Skills can support additional metadata; keep it conservative.
|
|
10
|
+
fm = dump_frontmatter(
|
|
11
|
+
{
|
|
12
|
+
"name": spec.name,
|
|
13
|
+
"description": spec.description,
|
|
14
|
+
"compatibility": "Multi-assistant (Claude/Windsurf/Cursor/GitHub Skills)",
|
|
15
|
+
"metadata": "Generated from canonical skillspec.json",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
body = []
|
|
20
|
+
body.append("# " + spec.name)
|
|
21
|
+
body.append("")
|
|
22
|
+
body.append("## Overview")
|
|
23
|
+
body.append(spec.description)
|
|
24
|
+
body.append("")
|
|
25
|
+
if spec.triggers:
|
|
26
|
+
body.append("## Triggers")
|
|
27
|
+
for t in spec.triggers:
|
|
28
|
+
body.append(f"- {t}")
|
|
29
|
+
body.append("")
|
|
30
|
+
body.append("## Workflow")
|
|
31
|
+
body.append(f"*Mode:* `{spec.workflow_type}`")
|
|
32
|
+
body.append("")
|
|
33
|
+
body.append(_render_steps(spec.steps))
|
|
34
|
+
body.append("")
|
|
35
|
+
body.append(_render_resources(spec))
|
|
36
|
+
body.append("## Output contract")
|
|
37
|
+
body.append("- Deterministic artifacts (files, zips, reports) whenever possible")
|
|
38
|
+
body.append("- Machine-readable output available when scripts support `--json`")
|
|
39
|
+
body.append(_render_footer())
|
|
40
|
+
return fm + "\n".join(body)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..spec.model import SkillSpec
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_windsurf_workflow_md(spec: SkillSpec) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Render Windsurf workflow markdown (PRIMARY target).
|
|
9
|
+
|
|
10
|
+
Follows ui-ux-pro-max patterns:
|
|
11
|
+
- Clear workflow steps with numbered sections
|
|
12
|
+
- Gate steps marked with 🚦
|
|
13
|
+
- References expanded to full paths
|
|
14
|
+
- Deterministic tools section for scripts
|
|
15
|
+
"""
|
|
16
|
+
# YAML frontmatter
|
|
17
|
+
lines = [
|
|
18
|
+
"---",
|
|
19
|
+
f"description: {spec.description}",
|
|
20
|
+
"auto_execution_mode: 3",
|
|
21
|
+
"---",
|
|
22
|
+
"",
|
|
23
|
+
f"# {spec.name}",
|
|
24
|
+
"",
|
|
25
|
+
spec.description,
|
|
26
|
+
"",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Prerequisites (if scripts exist)
|
|
30
|
+
if spec.scripts:
|
|
31
|
+
lines.extend([
|
|
32
|
+
"## Prerequisites",
|
|
33
|
+
"",
|
|
34
|
+
"```bash",
|
|
35
|
+
"python3 --version || python --version",
|
|
36
|
+
"```",
|
|
37
|
+
"",
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
# Triggers
|
|
41
|
+
if spec.triggers:
|
|
42
|
+
lines.append("## Triggers")
|
|
43
|
+
lines.append("")
|
|
44
|
+
for t in spec.triggers:
|
|
45
|
+
lines.append(f"- {t}")
|
|
46
|
+
lines.append("")
|
|
47
|
+
|
|
48
|
+
# How to Use (for skill-creator specifically)
|
|
49
|
+
is_skill_creator = spec.name == "skill-creator"
|
|
50
|
+
if is_skill_creator:
|
|
51
|
+
lines.extend([
|
|
52
|
+
"## How to Use This Workflow",
|
|
53
|
+
"",
|
|
54
|
+
"When user requests skill creation (create, build, generate, update), follow this workflow:",
|
|
55
|
+
"",
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
# Workflow
|
|
59
|
+
lines.append("## Workflow")
|
|
60
|
+
lines.append("")
|
|
61
|
+
lines.append(f"Mode: `{spec.workflow_type}`")
|
|
62
|
+
lines.append("")
|
|
63
|
+
|
|
64
|
+
for i, step in enumerate(spec.steps, start=1):
|
|
65
|
+
kind_badge = " 🚦 GATE" if step.kind == "gate" else ""
|
|
66
|
+
lines.append(f"### Step {i}: {step.title}{kind_badge}")
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
if step.notes:
|
|
70
|
+
notes = step.notes
|
|
71
|
+
# Expand references paths
|
|
72
|
+
notes = notes.replace("`references/", f"`skills/{spec.name}/references/")
|
|
73
|
+
lines.append(notes)
|
|
74
|
+
lines.append("")
|
|
75
|
+
|
|
76
|
+
if step.commands:
|
|
77
|
+
lines.append("```bash")
|
|
78
|
+
for cmd in step.commands:
|
|
79
|
+
lines.append(cmd)
|
|
80
|
+
lines.append("```")
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
# References section
|
|
84
|
+
if spec.references:
|
|
85
|
+
lines.append("## References")
|
|
86
|
+
lines.append("")
|
|
87
|
+
for ref in spec.references:
|
|
88
|
+
ref_path = f"skills/{spec.name}/{ref}"
|
|
89
|
+
lines.append(f"- `{ref_path}`")
|
|
90
|
+
lines.append("")
|
|
91
|
+
|
|
92
|
+
# Deterministic tools (for skill-creator)
|
|
93
|
+
if is_skill_creator:
|
|
94
|
+
lines.extend([
|
|
95
|
+
"## Search & Generate Tools",
|
|
96
|
+
"",
|
|
97
|
+
"```bash",
|
|
98
|
+
"# Search skill patterns and get recommendations",
|
|
99
|
+
'python3 .shared/skill-creator/scripts/search.py "<keywords>" --skill-system -p "<name>"',
|
|
100
|
+
"",
|
|
101
|
+
"# Generate Windsurf workflow (primary target)",
|
|
102
|
+
"python3 .shared/skill-creator/scripts/generate.py skills/<skill>/skillspec.json",
|
|
103
|
+
"",
|
|
104
|
+
"# Generate all targets (backward compat)",
|
|
105
|
+
"python3 .shared/skill-creator/scripts/generate.py skills/<skill>/skillspec.json --all",
|
|
106
|
+
"",
|
|
107
|
+
"# Validate spec and outputs",
|
|
108
|
+
"python3 .shared/skill-creator/scripts/validate.py skills/<skill>/skillspec.json",
|
|
109
|
+
"",
|
|
110
|
+
"# Package Claude .skill",
|
|
111
|
+
"python3 .shared/skill-creator/scripts/package.py --target claude --skill <skill>",
|
|
112
|
+
"```",
|
|
113
|
+
"",
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
# Footer
|
|
117
|
+
lines.extend([
|
|
118
|
+
"---",
|
|
119
|
+
"",
|
|
120
|
+
f"*Generated by skill-creator. Source: `skills/{spec.name}/skillspec.json`*",
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def dump_frontmatter(fields: Dict[str, Any]) -> str:
|
|
7
|
+
"""Dump a minimal YAML frontmatter block.
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- We intentionally keep this implementation small and dependency-free.
|
|
11
|
+
- Values are rendered as single-line scalars (quoted when needed).
|
|
12
|
+
"""
|
|
13
|
+
lines = ["---"]
|
|
14
|
+
for k, v in fields.items():
|
|
15
|
+
if v is None:
|
|
16
|
+
continue
|
|
17
|
+
s = str(v)
|
|
18
|
+
# Quote if it contains colon, newline, or leading/trailing whitespace
|
|
19
|
+
if any(ch in s for ch in [":", "\n"]) or s != s.strip():
|
|
20
|
+
s = s.replace("\\", "\\\\").replace('"', '\\"')
|
|
21
|
+
s = f'"{s}"'
|
|
22
|
+
lines.append(f"{k}: {s}")
|
|
23
|
+
lines.append("---")
|
|
24
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ensure_dir(path: Path) -> None:
|
|
10
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_json(path: Path) -> Dict[str, Any]:
|
|
14
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def write_json(path: Path, obj: Any) -> None:
|
|
18
|
+
ensure_dir(path.parent)
|
|
19
|
+
path.write_text(json.dumps(obj, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def copytree(src: Path, dst: Path, *, overwrite: bool = True) -> None:
|
|
23
|
+
if not src.exists():
|
|
24
|
+
return
|
|
25
|
+
if dst.exists() and overwrite:
|
|
26
|
+
shutil.rmtree(dst)
|
|
27
|
+
shutil.copytree(src, dst)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def copyfile(src: Path, dst: Path) -> None:
|
|
31
|
+
ensure_dir(dst.parent)
|
|
32
|
+
shutil.copy2(src, dst)
|