@opvs-ai/skills
v0.7.3
Published
opvsHUB Skill Factory + marketplace CLI — generate plugins from schema.yaml; publish / install / doctor signed marketplace packages
Readme
@opvs-ai/skills
Skill Factory for opvsHUB — generates typed OpenClaw gateway plugins from schema.yaml API definitions.
Install
# From the private OPVS registry
npm install @opvs-ai/skills --registry https://npm.opvs.aiUsage
Generate plugins from schema.yaml files
# Generate a single skill
opvs-skills generate agentboard --skills-dir ./skills --plugins-dir ./plugins
# Generate all skills
opvs-skills generate --all --skills-dir ./skills --plugins-dir ./plugins
# Generate all + bundle for gateway deployment
opvs-skills generate --all \
--skills-dir ./skills \
--plugins-dir ./plugins \
--bundle ./bundle/index.ts
# Generate all + MCP tool definitions
opvs-skills generate --all \
--skills-dir ./skills \
--plugins-dir ./plugins \
--mcp ./mcp/generated-tools.tsValidate schemas
opvs-skills validate --skills-dir ./skillsList skills with tier status
opvs-skills list --skills-dir ./skillsEmit runtime-native skill bundles (W2.3)
emit-runtime re-emits opvsHUB skills as runtime-native skill/rule
bundles for non-OpenClaw agent runtimes. Each runtime has its own
filesystem convention; the generator handles the path + frontmatter
shape per runtime.
| Runtime | Drop path inside --out | Frontmatter shape |
|---|---|---|
| claude-code | .claude/skills/<prefix>-<name>/SKILL.md | name, description (per Claude Code skills) |
| antigravity | .agent/skills/<prefix>-<name>/SKILL.md | name, description (per Antigravity skills codelab) |
| cursor | .cursor/rules/<prefix>-<name>.mdc | description, alwaysApply (per Cursor rules) |
| windsurf | .windsurf/rules/<prefix>-<name>.md (capped at 12,000 chars) | trigger, description (per Windsurf memories) |
# Emit a single skill for Claude Code
opvs-skills emit-runtime claude-code \
--skills-dir ./skills \
--out ./opvs-runtime-bundle/claude \
--skill vayapin
# Emit all skills for Cursor
opvs-skills emit-runtime cursor \
--skills-dir ./skills \
--out ./opvs-runtime-bundle/cursor
# Restrict to an allowlist
opvs-skills emit-runtime windsurf \
--skills-dir ./skills \
--out ./opvs-runtime-bundle/windsurf \
--allowlist vayapin,agentboard,opvs-foundation
# Dry-run (print what would be written without touching disk)
opvs-skills emit-runtime antigravity \
--skills-dir ./skills \
--out /tmp/scratch \
--dry-run--prefix <prefix> controls the filename / directory prefix (default
opvs). The user drops the contents of --out into a target project
root to activate the skills.
End-to-end verification status (2026-05-15):
| Runtime | Status |
|---|---|
| claude-code | Live-verified — fresh project + dropped bundle → claude --print discovers /opvs-vayapin, lists methods + env vars. Session log: tests/fixtures/e2e/claude-code-session.md |
| cursor | Verified via Claude-equivalent rule-injection simulation — rule body fed as system prompt yields a correct, contextual answer. Session log: tests/fixtures/e2e/cursor-session.md |
| windsurf | Format-level verified only — file lands at .windsurf/rules/, frontmatter parses, body kept under the 12k char cap. IDE-side activation untested (Windsurf is GUI-only). |
| antigravity | Format-level verified only — file lands at .agent/skills/<slug>/SKILL.md, frontmatter matches the docs. IDE-side activation untested. |
What it generates
From a single schema.yaml:
| Output | Purpose |
|--------|---------|
| {skill}.ts plugin | TypeScript plugin with api.registerTool() calls — native LLM tools |
| skills.yaml manifest | Tier 1 discovery index (one line per skill) |
| index.ts bundle | All plugins merged into one file for gateway deployment |
| generated-tools.ts | MCP tool definitions for the @opvs-ai/mcp server |
schema.yaml format
name: my-skill
version: "1.0.0"
description: What this skill does
category: utility
base_url: /api/v1/my-service
auth:
type: pat
header: Authorization
prefix: Bearer
env_var: OPVS_PAT
methods:
listItems:
description: List all items
http: GET /items
params:
status:
type: enum
values: [active, archived]
required: false
response:
format: yamlSee the opvsHUB repo for all skill schemas and full documentation.
Programmatic API
import { generateFromSchema } from "@opvs-ai/skills";
const schemaYaml = fs.readFileSync("schema.yaml", "utf-8");
const result = generateFromSchema(schemaYaml);
// result.plugin — TypeScript plugin code
// result.manifest — skills.yaml entry
// result.skillMd — updated SKILL.md (if applicable)OpenAPI ↔ schema.yaml drift detection (check / fix)
A.4 / A.6 in B1 Bundle 1 (2026-05-22) were both release-process bugs — agentboard_reorderColumns had a stale param shape and agentboard_updateColumn was missing from the published MCP entirely. Both were caused by schema.yaml drifting from the live FastAPI OpenAPI spec with nobody noticing until customer-facing tools 422'd.
opvs-skills check is the gate that prevents this from happening again. Point it at any FastAPI service's /openapi.json and it compares the live spec to your schema.yaml on a per-operation basis.
# Local smoke (boot the service first):
docker compose --profile queue up -d agentboard
sleep 20
opvs-skills check --skill agentboard --against http://localhost:8100/openapi.json
# Exit 1 on drift + structured report on stdout
# Exit 0 + "All schema methods match the live OpenAPI spec." when clean
# Against a checked-in OpenAPI export (no service boot needed):
opvs-skills check --skill agentboard --against ./openapi-agentboard.json
# JSON output for machine consumption:
opvs-skills check --skill agentboard --against http://localhost:8100/openapi.json --format json
# Skip known unbacked operations (e.g. routes intentionally not on the agent surface):
opvs-skills check --skill agentboard --against ... --ignore "GET /docs,GET /openapi.json"The diff reports four drift classes per operation:
- schema_only_fields — schema declares a param the API doesn't accept (
column_idsafter the rename) - api_only_fields — API requires a param the schema is missing (
columnsafter the rename) - required_mismatch — both sides know the field but disagree on
required - type_mismatch — both sides know the field but disagree on primitive type
Plus two operation-level diffs:
- schema_only_op — schema declares an
http:route the API doesn't expose - api_only_op — API exposes a route no schema method binds to (this is what would have caught
updateColumn)
fix — emit a hand-apply checklist
opvs-skills fix --skill agentboard --against http://localhost:8100/openapi.json --out fix.mdfix deliberately does NOT mutate schema.yaml in place. Descriptions are hand-crafted and an automated rewrite risks stamping over them. The output is a markdown checklist with concrete per-field instructions you can copy into the YAML by hand.
Which drift FAILS: --fail-on
The six classes above split into three things, not two, and conflating any of them is what kept this check advisory for months.
| | Classes | Meaning | Gates? |
|---|---|---|---|
| blocking | schema_only_fields, required_mismatch, type_mismatch, schema_only_op | The schema is wrong. An agent that follows it literally gets a silently-dropped write, a 422, or a 404. | always |
| unexposed fields | api_only_fields on a bound method | The agent can reach the endpoint and cannot set a field the API accepts. A capability gap in a surface already committed to. | at fields |
| unwrapped routes | api_only_op | A route the skill does not bind. Curated-subset scope, not a defect. | never |
# Default — every difference exits 1. Useful locally; unusable as a gate.
opvs-skills check --skill agentboard --against ...
# Gate form — WRONG, plus an undecided field on a method you already bind.
opvs-skills check --skill agentboard --against ... --fail-on fields
# Narrowest — only WRONG.
opvs-skills check --skill agentboard --against ... --fail-on mismatch🔴 --fail-on any can never be green for any real skill, and that is not a
bug in your schema. A skill wraps a curated subset of its service by design
— admin-catalog binds 44 of 1117 operations — so api_only_op is the normal
state, not a defect. Requiring full coverage would force exposing internal
config (default_model, model_overrides, heartbeat_config) to a
vendor-facing agent surface just to satisfy CI.
🔑 Why fields is a separate rung (OTAS-36). Until the split, both coverage
classes shared one counter — and they move in opposite directions. For
admin-catalog that number read 1084: 1073 unwrapped routes it should never
try to reduce, hiding 11 bound methods that were genuinely missing fields.
Nobody could act on the sum, so nobody did. Separated, the second half is
drivable to zero — and two of the three gated services (agentboard,
agentmemory) were already there when it was first measured.
A withheld field is a decision, and the decision has to be written down.
Record it in _drift_ignore.field_mismatches with a comment saying why:
_drift_ignore:
field_mismatches:
# `rating` — the star rating on a public storefront. Must DERIVE from job
# history; a hand-set value is fabricated social proof.
# `heartbeat_config` — runtime scheduling, owned by the gateway.
updateProfile: [rating, heartbeat_config]A method whose every finding is allowlisted is promoted back to match, so the
gate goes green. An undeclared withhold is indistinguishable from an oversight
and fails — which is the point: a field added to the API tomorrow blocks the
merge until someone decides expose-or-withhold.
CI gate
The .github/workflows/tests.yml workflow
runs opvs-skills check --fail-on fields against three services on every PR:
agentboard, agentmemory, admin-catalog. Since OTAS-32 it is a
required check in the All Tests Pass gate; since OTAS-36 it holds both
blocking drift and unexposed fields at zero to merge. This is what catches
A.4/A.6-class regressions before they hit npm publish.
It was advisory from B1 until 2026-08-27 on the plan that a follow-up would
walk the report and then flip it. That follow-up could not have worked: the
check exited 1 on any unwrapped route, so no amount of reconciliation would have
turned it green. Measured at the flip — agentboard 0 blocking / 25 coverage,
agentmemory 0 / 10, admin-catalog 0 / 1084 — the two services long believed to
hold "unreconciled drift" had never had a single field-level defect. The earlier
version of this section also named agentdocs, which has never been in the
matrix. OTAS-36 then split that coverage figure and raised the gate to
--fail-on fields; at that flip the unexposed-field counts were agentboard 0,
agentmemory 0, admin-catalog 11 → 0.
Requirements
- Node.js >= 18
- OpenClaw gateway for plugin deployment
