Files
ponytail/scripts/build-openclaw-skills.js
T
Emeriko 0e3fd0cfee Trigger ponytail skill on any coding task, not just keywords
The skill description was written keyword-first ("use when the user
says lazy / complains about boilerplate"), so a model-invoked host
only loaded it on prompts that hit those words. Plain coding tasks
(add a date picker, write a dedupe function, build a cache, fix a bug)
were skipped, exactly the over-build cases ponytail is best at.

Tested via a skill-router proxy (3 runs, 12 labeled prompts): recall
on coding tasks was 2/6 before, 6/6 after, with precision unchanged at
6/6 (no false fires on non-coding prompts). Fix is one clause naming
coding tasks explicitly plus a negative clause to keep precision.

Trigger descriptions live in two source-of-truth spots, both updated:
skills/ponytail/SKILL.md and the DESCRIPTIONS map in
scripts/build-openclaw-skills.js (.openclaw regenerated). The store
blurbs in the plugin manifests are listing metadata, not triggers, so
they are left alone.
2026-06-29 07:06:59 +02:00

61 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
// Generate the OpenClaw / ClawHub skill package (.openclaw/skills/) from the
// canonical skills/. OpenClaw skills are SKILL.md (frontmatter + body), the same
// format ponytail already uses, with one difference: `description` must be a
// single line under 160 chars. The canonical descriptions are long (tuned for
// Claude's skill picker), so each ships a short one here. The body is copied
// verbatim from skills/<name>/SKILL.md so the ruleset never drifts; only the
// frontmatter is rewritten.
//
// Run: node scripts/build-openclaw-skills.js
// tests/openclaw-skills.test.js fails if the committed copies are stale.
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
const DESCRIPTIONS = {
'ponytail': 'Lazy senior dev mode for any coding task (write, refactor, fix, review): YAGNI, stdlib first, no unrequested abstractions. Not for non-coding requests.',
'ponytail-review': 'Review a diff for over-engineering. Finds what to delete: reinvented stdlib, needless deps, speculative abstractions. One line per finding.',
'ponytail-audit': 'Audit the whole repo for over-engineering. A ranked list of what to delete, simplify, or replace with stdlib or native features.',
'ponytail-debt': 'Harvest every ponytail: shortcut comment into one debt ledger, so deferrals get tracked instead of forgotten. One-shot report.',
'ponytail-gain': 'Show ponytail measured impact as a scoreboard: less code, less cost, more speed, from the benchmark medians. One-shot display.',
'ponytail-help': "Quick reference for ponytail's modes, skills, and commands. One-shot display.",
};
const NAMES = Object.keys(DESCRIPTIONS);
function sourceBody(name) {
const src = fs.readFileSync(path.join(ROOT, 'skills', name, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n');
const fm = src.match(/^---\n[\s\S]*?\n---\n?/);
if (!fm) throw new Error(`skills/${name}/SKILL.md has no frontmatter`);
return src.slice(fm[0].length);
}
function render(name) {
const desc = DESCRIPTIONS[name];
if (desc.length > 160 || desc.includes('\n') || desc.includes('"')) {
throw new Error(`description for ${name} must be one line, no quotes, under 160 chars`);
}
const frontmatter =
`---\nname: ${name}\ndescription: "${desc}"\nhomepage: ${HOMEPAGE}\nlicense: MIT\n---\n`;
return frontmatter + sourceBody(name);
}
function outPath(name) {
return path.join(ROOT, '.openclaw', 'skills', name, 'SKILL.md');
}
module.exports = { DESCRIPTIONS, NAMES, render, outPath, sourceBody };
if (require.main === module) {
for (const name of NAMES) {
const p = outPath(name);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, render(name));
console.log('wrote', path.relative(ROOT, p).replace(/\\/g, '/'));
}
}