skill-creator
Create, update, validate, and evaluate Wisp skills. Use when authoring a project-local or installable skill, refining its trigger description, adding deterministic scripts or Python sidecars, or testing whether another Agent can follow the workflow.
Install
npx skills add https://github.com/xuzhougeng/wisp-science/tree/main/skills/skill-creator
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install xuzhougeng-wisp-science@llmmart
git clone https://github.com/xuzhougeng/wisp-science.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole xuzhougeng/wisp-science collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Create Wisp skills
Author project-local skills under .wisp/skills/<name>/. Wisp also discovers
bundled skills, user-installed skills, and paths configured by
WISP_SKILLS_PATH, but only normal project paths are directly writable through
Agent file tools.
Structure
<skill-name>/
├── SKILL.md # frontmatter trigger + procedure body
├── runtime.py # optional helpers for the persistent Python runtime
├── runtime.r # optional helpers for the persistent R runtime
├── scripts/ # optional standalone deterministic programs
├── references/ # optional detailed domain material
└── assets/ # optional output templates or static inputs
Keep SKILL.md concise. Put triggering information in frontmatter
description; put essential procedure in the body; move detailed variants to
one-level-deep references. Add only resources the workflow actually uses.
Workflow
- Define concrete user requests that should trigger the skill and the expected outputs.
- Search existing skills before creating a duplicate.
- Choose a lowercase hyphenated name and create
.wisp/skills/<name>/SKILL.mdwithwrite. - Add reusable scripts before writing long inline code examples. Execute every new script on representative local data.
- Add root-level
runtime.pyand/orruntime.rwhen helpers need to work with persistent interpreter state. The rendered skill supplies a one-time loading instruction for each file:exec(compile(...))throughpython, orsource(..., local = TRUE)throughr. Skill loading itself does not execute them or inject Wisp tools. These files run inside the selected runtime; do not invoke them as standalone CLI scripts. - Validate structure with this skill's
scripts/quick_validate.py <skill-directory>. - Refresh or reopen the project if the new skill does not yet appear, then find
it with
search_skillsand load it withuse_skill. - Exercise the skill on realistic tasks. When explicit Wisp delegation is available, use a fresh bounded task with only the skill path and user-style request; do not leak the expected answer into the evaluation prompt.
For a user-wide installation, ask the user to install the validated folder via Settings → Skills. There is no Agent-side publish, overwrite, or delete API.
Frontmatter
At minimum include:
---
name: my-skill
description: Perform X. Use when the user asks for Y, Z, or related output.
---
The folder name and name should match. The description is the primary trigger;
state both what the skill does and when it should be selected.
Runtime sidecar rules
Keep top-level code definition-only:
- allow imports, function definitions, and literal constant assignments;
- defer optional third-party imports into function bodies;
- do not run work, access the network, or modify files at load time;
- do not depend on injected Agent, Run, credential, artifact, or model objects;
- pass paths and configuration explicitly;
- use the corresponding
pythonorrtool after loading; reload when that runtime restarts or the conversation/execution context changes; - Python and R retain separate state; use prefixed names to avoid collisions with other helpers within each language's namespace;
- Python loading runs in
__main__, so put self-checks in explicitly called functions rather than a__main__guard.
Use scripts/ when a helper is a standalone CLI, needs argument parsing, or
should run through run_in_context. Choose by state reuse and execution needs,
not file length. scripts/runtime.py and scripts/runtime.r are ordinary
scripts; only the reserved root-level filenames get runtime loading guidance.
Bundled scripts
scripts/quick_validate.py <skill-dir>— checks frontmatter shape, kebab-case naming, folder/name match, and length limits; prints every problem at once.scripts/package_skill.py <skill-dir> [out-dir]— validates, then zips the folder into<name>.skillfor user-wide installation, skipping build junk and a root-levelevals/folder.
To evaluate a skill, run it on realistic tasks yourself (step 8 above) and keep evaluation artifacts out of the skill folder unless they are intentional reusable resources.
Wisp boundaries
search_skillsanduse_skilldo not edit the catalog.- Project file tools cannot manage user-wide installed skills outside granted workspace paths.
run_in_contextexecutes deterministic work; it does not publish skills or call models.- Specialist creation is separate. Load
customizeand usesave_specialistonly when that explicit tool is advertised.
Files (wisp-science)
-
scripts
-
package_skill.py 2.1 KB
#!/usr/bin/env python3 """Zip a validated skill folder into a distributable `<name>.skill` archive. Usage: python package_skill.py <skill-directory> [output-directory] Validates first (via quick_validate.check_skill), then writes the archive with the skill folder name as the top-level entry. Build junk (__pycache__, node_modules, *.pyc, .DS_Store) and a root-level `evals/` folder are skipped. """ import sys import zipfile from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from quick_validate import check_skill SKIP_ANYWHERE = {"__pycache__", "node_modules", ".DS_Store"} SKIP_SUFFIXES = {".pyc"} SKIP_AT_ROOT = {"evals"} def _included(rel): """rel is relative to the skill folder itself.""" if rel.parts and rel.parts[0] in SKIP_AT_ROOT: return False if set(rel.parts) & SKIP_ANYWHERE: return False return rel.suffix not in SKIP_SUFFIXES def package(skill_dir, out_dir=None): """Return the written archive path, raising ValueError on a bad skill.""" skill_dir = Path(skill_dir).resolve() problems = check_skill(skill_dir) if problems: raise ValueError("; ".join(problems)) out_dir = Path(out_dir).resolve() if out_dir else Path.cwd() out_dir.mkdir(parents=True, exist_ok=True) archive = out_dir / f"{skill_dir.name}.skill" files = sorted( p for p in skill_dir.rglob("*") if p.is_file() and _included(p.relative_to(skill_dir)) ) with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: for p in files: zf.write(p, Path(skill_dir.name) / p.relative_to(skill_dir)) return archive, files def main(argv): if len(argv) not in (2, 3): print(__doc__.strip().splitlines()[2]) return 1 try: archive, files = package(argv[1], argv[2] if len(argv) == 3 else None) except ValueError as e: print(f"validation failed: {e}") return 1 for p in files: print(f" + {Path(p).relative_to(Path(argv[1]).resolve())}") print(f"wrote {archive} ({len(files)} files)") return 0 if __name__ == "__main__": sys.exit(main(sys.argv)) -
quick_validate.py 4.3 KB
#!/usr/bin/env python3 """Validate a Wisp skill folder: frontmatter shape, naming, and length limits. Usage: python quick_validate.py <skill-directory> Prints every problem found (not just the first) and exits non-zero if any. """ import re import sys from pathlib import Path KNOWN_KEYS = { "name", "description", "license", "allowed-tools", "metadata", "compatibility", "fold_cue", "wisp", } NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") MAX_NAME = 64 MAX_DESCRIPTION = 1024 MAX_COMPATIBILITY = 500 def _parse_minimal(block): """Fallback frontmatter parser for machines without pyyaml. Handles what skill frontmatter actually uses: top-level `key: value` pairs, quoted scalars, and `>`/`|` block scalars. Nested values are kept only as opaque markers since validation never inspects them. """ lines = block.split("\n") data, i = {}, 0 while i < len(lines): m = re.match(r"^([A-Za-z][\w-]*):\s*(.*)$", lines[i]) if not m: i += 1 continue key, rest = m.group(1), m.group(2).strip() i += 1 body = [] while i < len(lines) and (not lines[i].strip() or lines[i].startswith((" ", "\t"))): body.append(lines[i].strip()) i += 1 if rest in (">", "|", ">-", "|-", ""): data[key] = " ".join(filter(None, body)) if body else {} else: data[key] = rest.strip("'\"") return data def _frontmatter(text): """Return (dict, error) for the YAML block between the leading --- fences.""" m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) if not m: return None, "SKILL.md must open with a `---` fenced YAML frontmatter block" try: import yaml except ModuleNotFoundError: return _parse_minimal(m.group(1)), None try: data = yaml.safe_load(m.group(1)) except yaml.YAMLError as e: return None, f"frontmatter is not valid YAML: {e}" if not isinstance(data, dict): return None, "frontmatter must be a YAML mapping" return data, None def check_skill(skill_dir): """Return a list of problem strings; empty list means the skill passes.""" skill_dir = Path(skill_dir) skill_md = skill_dir / "SKILL.md" if not skill_md.is_file(): return [f"{skill_md} does not exist"] fm, err = _frontmatter(skill_md.read_text(encoding="utf-8")) if err: return [err] problems = [] problems += [ f"unknown frontmatter key `{k}` (known: {', '.join(sorted(KNOWN_KEYS))})" for k in sorted(set(fm) - KNOWN_KEYS) ] name = fm.get("name") if not isinstance(name, str) or not name.strip(): problems.append("frontmatter needs a non-empty string `name`") else: name = name.strip() if not NAME_RE.fullmatch(name): problems.append( f"`name: {name}` must be kebab-case: lowercase/digit runs joined by single hyphens" ) if len(name) > MAX_NAME: problems.append(f"`name` exceeds {MAX_NAME} chars ({len(name)})") if name != skill_dir.resolve().name: problems.append( f"`name: {name}` should match its folder `{skill_dir.resolve().name}`" ) desc = fm.get("description") if not isinstance(desc, str) or not desc.strip(): problems.append("frontmatter needs a non-empty string `description`") else: if len(desc) > MAX_DESCRIPTION: problems.append(f"`description` exceeds {MAX_DESCRIPTION} chars ({len(desc)})") if "<" in desc or ">" in desc: problems.append("`description` must not contain angle brackets") compat = fm.get("compatibility") if compat is not None: if not isinstance(compat, str): problems.append("`compatibility` must be a string when present") elif len(compat) > MAX_COMPATIBILITY: problems.append(f"`compatibility` exceeds {MAX_COMPATIBILITY} chars ({len(compat)})") return problems def main(argv): if len(argv) != 2: print(__doc__.strip().splitlines()[2]) return 1 problems = check_skill(argv[1]) for p in problems: print(f"FAIL: {p}") if not problems: print("OK: skill passes validation") return 1 if problems else 0 if __name__ == "__main__": sys.exit(main(sys.argv))
-
-
SKILL.md 4.9 KB
--- name: skill-creator description: Create, update, validate, and evaluate Wisp skills. Use when authoring a project-local or installable skill, refining its trigger description, adding deterministic scripts or Python/R runtime sidecars, or testing whether another Agent can follow the workflow. --- # Create Wisp skills Author project-local skills under `.wisp/skills/<name>/`. Wisp also discovers bundled skills, user-installed skills, and paths configured by `WISP_SKILLS_PATH`, but only normal project paths are directly writable through Agent file tools. ## Structure ```text <skill-name>/ ├── SKILL.md # frontmatter trigger + procedure body ├── runtime.py # optional helpers for the persistent Python runtime ├── runtime.r # optional helpers for the persistent R runtime ├── scripts/ # optional standalone deterministic programs ├── references/ # optional detailed domain material └── assets/ # optional output templates or static inputs ``` Keep `SKILL.md` concise. Put triggering information in frontmatter `description`; put essential procedure in the body; move detailed variants to one-level-deep references. Add only resources the workflow actually uses. ## Workflow 1. Define concrete user requests that should trigger the skill and the expected outputs. 2. Search existing skills before creating a duplicate. 3. Choose a lowercase hyphenated name and create `.wisp/skills/<name>/SKILL.md` with `write`. 4. Add reusable scripts before writing long inline code examples. Execute every new script on representative local data. 5. Add root-level `runtime.py` and/or `runtime.r` when helpers need to work with persistent interpreter state. The rendered skill supplies a one-time loading instruction for each file: `exec(compile(...))` through `python`, or `source(..., local = TRUE)` through `r`. Skill loading itself does not execute them or inject Wisp tools. These files run inside the selected runtime; do not invoke them as standalone CLI scripts. 6. Validate structure with this skill's `scripts/quick_validate.py <skill-directory>`. 7. Refresh or reopen the project if the new skill does not yet appear, then find it with `search_skills` and load it with `use_skill`. 8. Exercise the skill on realistic tasks. When explicit Wisp delegation is available, use a fresh bounded task with only the skill path and user-style request; do not leak the expected answer into the evaluation prompt. For a user-wide installation, ask the user to install the validated folder via **Settings → Skills**. There is no Agent-side publish, overwrite, or delete API. ## Frontmatter At minimum include: ```yaml --- name: my-skill description: Perform X. Use when the user asks for Y, Z, or related output. --- ``` The folder name and `name` should match. The description is the primary trigger; state both what the skill does and when it should be selected. ## Runtime sidecar rules Keep top-level code definition-only: - allow imports, function definitions, and literal constant assignments; - defer optional third-party imports into function bodies; - do not run work, access the network, or modify files at load time; - do not depend on injected Agent, Run, credential, artifact, or model objects; - pass paths and configuration explicitly; - use the corresponding `python` or `r` tool after loading; reload when that runtime restarts or the conversation/execution context changes; - Python and R retain separate state; use prefixed names to avoid collisions with other helpers within each language's namespace; - Python loading runs in `__main__`, so put self-checks in explicitly called functions rather than a `__main__` guard. Use `scripts/` when a helper is a standalone CLI, needs argument parsing, or should run through `run_in_context`. Choose by state reuse and execution needs, not file length. `scripts/runtime.py` and `scripts/runtime.r` are ordinary scripts; only the reserved root-level filenames get runtime loading guidance. ## Bundled scripts - `scripts/quick_validate.py <skill-dir>` — checks frontmatter shape, kebab-case naming, folder/name match, and length limits; prints every problem at once. - `scripts/package_skill.py <skill-dir> [out-dir]` — validates, then zips the folder into `<name>.skill` for user-wide installation, skipping build junk and a root-level `evals/` folder. To evaluate a skill, run it on realistic tasks yourself (step 8 above) and keep evaluation artifacts out of the skill folder unless they are intentional reusable resources. ## Wisp boundaries - `search_skills` and `use_skill` do not edit the catalog. - Project file tools cannot manage user-wide installed skills outside granted workspace paths. - `run_in_context` executes deterministic work; it does not publish skills or call models. - Specialist creation is separate. Load `customize` and use `save_specialist` only when that explicit tool is advertised.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.