@vegastack/skills 0.13.0 → 0.14.0
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/README.md +45 -8
- package/dist/index.js +184 -67
- package/package.json +1 -1
- package/skill/skill-maintainer/SKILL.md +3 -1
- package/skill/skill-maintainer/references/standards.md +2 -0
- package/skill/skill-maintainer/refresh/REFRESH.md +2 -2
- package/skill/skillify/assets/templates/README.md.template +1 -1
- package/skill/skillify/refresh/REFRESH.md +1 -1
- package/skill/skillify/scripts/scaffold-skill.mjs +15 -4
- package/skill-integrity.json +30 -6
package/README.md
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
Installer for VegaStack Agent Skills — a family of self-contained skills for Claude Code, Codex, and Hermes, shipped in one integrity-checked package.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx @vegastack/skills list
|
|
7
|
-
npx @vegastack/skills add dev-
|
|
6
|
+
npx @vegastack/skills list # what is bundled, by group
|
|
7
|
+
npx @vegastack/skills add --group dev-skills # the whole dev workflow, one command
|
|
8
|
+
npx @vegastack/skills add dev-architect # or a single skill
|
|
8
9
|
```
|
|
9
10
|
|
|
10
11
|
## Skills in this package
|
|
11
12
|
|
|
13
|
+
### `dev-skills` — the issue-driven dev workflow
|
|
14
|
+
|
|
15
|
+
Install the family with `add --group dev-skills`.
|
|
16
|
+
|
|
12
17
|
| Skill | What it does |
|
|
13
18
|
|---|---|
|
|
14
|
-
| `skill-maintainer` | Encodes the Agent Skills standards (Claude Code, Codex, Hermes, agentskills.io) for creating, updating, and releasing skills in a skills repo |
|
|
15
|
-
| `skillify` | Turns a feature or workflow into a complete skill conforming to the VegaStack skills contract, or audits an existing one |
|
|
16
19
|
| `dev-setup` | Bootstraps any project, greenfield included, for the issue-driven dev workflow: stack-playbook-drafted profile, AGENTS.md section, labels, guards, decision register |
|
|
17
20
|
| `dev-intake` | Turns ideas, brainstorms, and SOWs into agent-ready GitHub issues with recorded user approval |
|
|
18
21
|
| `dev-plan` | Plans an approved issue before any code exists: fresh-grounded questionnaire, strict plan format with Interfaces blocks, the scope ratchet, quick-build inline mode |
|
|
@@ -24,26 +27,60 @@ npx @vegastack/skills add dev-architect
|
|
|
24
27
|
| `dev-status` | The operator's board: whose move is it, from deterministic gh data |
|
|
25
28
|
| `dev-chronicle` | The project's narrative record: story entries per branch and the "catch me up" digest |
|
|
26
29
|
|
|
30
|
+
### `repo-tooling` — repo-only
|
|
31
|
+
|
|
32
|
+
These operate on the vegastack-skills repository itself and do nothing useful in another project, so **`--all` skips them**. Install one by name if you are contributing to that repo.
|
|
33
|
+
|
|
34
|
+
| Skill | What it does |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `skill-maintainer` | Encodes the Agent Skills standards (Claude Code, Codex, Hermes, agentskills.io) for creating, updating, and releasing skills in a skills repo |
|
|
37
|
+
| `skillify` | Turns a feature or workflow into a complete skill conforming to the VegaStack skills contract, or audits an existing one |
|
|
38
|
+
|
|
27
39
|
## Commands
|
|
28
40
|
|
|
29
41
|
| Command | What it does |
|
|
30
42
|
|---|---|
|
|
31
43
|
| `list` | Show the bundled skills |
|
|
32
|
-
| `add <
|
|
33
|
-
| `verify [
|
|
44
|
+
| `add <selection>` | Install (or upgrade) skills into the selected agent directories |
|
|
45
|
+
| `verify [selection]` | Check installed copies against the bundled checksum manifest (all bundled skills when nothing is selected) |
|
|
34
46
|
| `doctor` | Diagnose an install: integrity across all skills, dev profile (`.vegastack/dev.md`) presence, installed-vs-latest version |
|
|
35
|
-
| `remove <
|
|
47
|
+
| `remove <selection>` | Uninstall skills from the selected agent directories |
|
|
48
|
+
|
|
49
|
+
### Selecting what to act on
|
|
50
|
+
|
|
51
|
+
`add`, `verify`, and `remove` each take **exactly one** selector. Combining two is an error, not a merge.
|
|
52
|
+
|
|
53
|
+
| Selector | Means |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `<skill>` | That one skill. Works for every bundled skill, repo-only ones included |
|
|
56
|
+
| `--group <group>` | Every skill in that group |
|
|
57
|
+
| `--all` | Every bundled skill **except** the repo-only ones |
|
|
58
|
+
|
|
59
|
+
A `--group` or `--all` install is **one transaction**: every skill is checked and staged before any of them is committed, so if one fails, none are installed and the destination is left exactly as it was.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx @vegastack/skills add --group dev-skills # the ten dev-workflow skills
|
|
63
|
+
npx @vegastack/skills add --all # everything worth installing in your project
|
|
64
|
+
npx @vegastack/skills verify --group dev-skills # check the family against the manifest
|
|
65
|
+
npx @vegastack/skills remove --group dev-skills # uninstall it again
|
|
66
|
+
```
|
|
36
67
|
|
|
37
68
|
## Flags
|
|
38
69
|
|
|
39
70
|
| Flag | Meaning |
|
|
40
71
|
|---|---|
|
|
41
72
|
| `--project` / `--global` | Install into the current project (default) or the user's home directory |
|
|
73
|
+
| `--group NAME` | Select every skill in a group (see `list` for the groups) |
|
|
74
|
+
| `--all` | Select every bundled skill except the repo-only ones |
|
|
42
75
|
| `--agent codex\|claude\|hermes\|both\|all` | Target agent runtime(s); `both` = codex+claude |
|
|
43
|
-
| `--dir PATH` | Operate on a different project directory |
|
|
76
|
+
| `--dir PATH` | Operate on a different project directory; not valid with `--global` |
|
|
44
77
|
| `--dry-run` | Show what would change without writing |
|
|
45
78
|
| `--force` | Overwrite a modified installed copy |
|
|
46
79
|
| `--non-interactive` | Skip prompts and use defaults: `--agent both`, project-local (for automation) |
|
|
80
|
+
| `--version` / `-v` | Print the installer version |
|
|
81
|
+
| `--help` / `-h` | Print usage |
|
|
82
|
+
|
|
83
|
+
`--all` and `--agent all` are different axes and are easy to confuse: `--all` chooses **which skills**, `--agent all` chooses **which agent runtimes**. `add --all --agent all --global` is valid and means every installable skill, on every runtime, in your home directory.
|
|
47
84
|
|
|
48
85
|
Agent targeting is automatic: the CLI detects which agents you have (`~/.claude`, `~/.codex`/`~/.agents`, `~/.hermes`) and targets them without asking — `--agent` overrides. A numbered picker appears only when nothing is detected. Installs are project-local by default; pass `--global` for the home directory (required for Hermes).
|
|
49
86
|
|
package/dist/index.js
CHANGED
|
@@ -3,20 +3,69 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { constants as fsConstants } from "node:fs";
|
|
6
|
-
import { access, cp, lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { access, cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
|
+
|
|
12
|
+
// src/selection.ts
|
|
13
|
+
var sorted = (names) => [...new Set(names)].sort();
|
|
14
|
+
function groupsIn(catalog) {
|
|
15
|
+
return sorted(catalog.map((entry) => entry.group).filter((group) => group !== null));
|
|
16
|
+
}
|
|
17
|
+
function selectSkills(selector, catalog, verb = "install") {
|
|
18
|
+
const chosen = [
|
|
19
|
+
selector.skill ? "a skill name" : null,
|
|
20
|
+
selector.group ? "--group" : null,
|
|
21
|
+
selector.all ? "--all" : null
|
|
22
|
+
].filter(Boolean);
|
|
23
|
+
if (chosen.length > 1) {
|
|
24
|
+
throw new Error(`Use only one of ${chosen.join(", ")} — they select different things and are not combined`);
|
|
25
|
+
}
|
|
26
|
+
if (selector.all) {
|
|
27
|
+
const installable = sorted(catalog.filter((entry) => !entry.repoOnly).map((entry) => entry.name));
|
|
28
|
+
if (!installable.length) {
|
|
29
|
+
throw new Error("Nothing to install: every bundled skill is repo-only. Name one explicitly, or use --group, to install it anyway.");
|
|
30
|
+
}
|
|
31
|
+
return installable;
|
|
32
|
+
}
|
|
33
|
+
if (selector.group) {
|
|
34
|
+
const members = sorted(catalog.filter((entry) => entry.group === selector.group).map((entry) => entry.name));
|
|
35
|
+
if (!members.length) {
|
|
36
|
+
const groups2 = groupsIn(catalog);
|
|
37
|
+
throw new Error(`Unknown group: ${selector.group}. Available groups: ${groups2.length ? groups2.join(", ") : "(none)"}`);
|
|
38
|
+
}
|
|
39
|
+
return members;
|
|
40
|
+
}
|
|
41
|
+
if (selector.skill) {
|
|
42
|
+
if (!catalog.some((entry) => entry.name === selector.skill)) {
|
|
43
|
+
throw new Error(`Unknown skill: ${selector.skill}. Bundled skills: ${sorted(catalog.map((entry) => entry.name)).join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
return [selector.skill];
|
|
46
|
+
}
|
|
47
|
+
const groups = groupsIn(catalog);
|
|
48
|
+
throw new Error(`Specify what to ${verb}: a skill name (${sorted(catalog.map((entry) => entry.name)).join(", ")})` + `${groups.length ? `, --group (${groups.join(", ")})` : ""}, or --all`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/index.ts
|
|
11
52
|
var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
53
|
var bundleRoot = join(packageRoot, "skill");
|
|
13
54
|
var surfaces = { codex: ".agents/skills", claude: ".claude/skills", hermes: ".hermes/skills" };
|
|
14
55
|
var packageVersion = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")).version;
|
|
15
56
|
function usage() {
|
|
16
57
|
return `Usage: vegastack-skills <add|verify|remove> <skill> [options]
|
|
58
|
+
vegastack-skills <add|verify|remove> --group <group> [options]
|
|
59
|
+
vegastack-skills <add|verify|remove> --all [options]
|
|
17
60
|
vegastack-skills <list|doctor> [options]
|
|
18
61
|
|
|
62
|
+
Select exactly one of: a skill name, --group <group>, or --all.
|
|
63
|
+
--all installs every skill except the repo-only ones; name those explicitly.
|
|
64
|
+
A --group or --all install is one transaction: if any skill fails, none are installed.
|
|
65
|
+
|
|
19
66
|
Options:
|
|
67
|
+
--group NAME install every skill in that group
|
|
68
|
+
--all every bundled skill except the repo-only ones
|
|
20
69
|
--agent codex|claude|hermes|both|all (both = codex+claude; hermes is global-only)
|
|
21
70
|
--project | --global
|
|
22
71
|
--dir PATH
|
|
@@ -34,12 +83,21 @@ async function bundledSkills() {
|
|
|
34
83
|
}
|
|
35
84
|
function parse(argv) {
|
|
36
85
|
const command = argv[0] && !argv[0].startsWith("-") ? argv.shift() : "help";
|
|
37
|
-
const options = { command, dryRun: false, force: false, nonInteractive: false };
|
|
86
|
+
const options = { command, all: false, dryRun: false, force: false, nonInteractive: false };
|
|
38
87
|
if (argv[0] && !argv[0].startsWith("-"))
|
|
39
88
|
options.skill = argv.shift();
|
|
40
89
|
while (argv.length) {
|
|
41
90
|
const flag = argv.shift();
|
|
42
|
-
if (flag === "--
|
|
91
|
+
if (flag === "--group") {
|
|
92
|
+
const value = argv.shift();
|
|
93
|
+
if (value === undefined || value === "" || value.startsWith("-"))
|
|
94
|
+
throw new Error("--group requires a value");
|
|
95
|
+
if (options.group !== undefined)
|
|
96
|
+
throw new Error("--group may be given only once; select one group per run");
|
|
97
|
+
options.group = value;
|
|
98
|
+
} else if (flag === "--all")
|
|
99
|
+
options.all = true;
|
|
100
|
+
else if (flag === "--agent")
|
|
43
101
|
options.agent = argv.shift();
|
|
44
102
|
else if (flag === "--project")
|
|
45
103
|
options.mode = "project";
|
|
@@ -68,13 +126,19 @@ function parse(argv) {
|
|
|
68
126
|
throw new Error("--dir cannot be combined with --global");
|
|
69
127
|
return options;
|
|
70
128
|
}
|
|
71
|
-
async function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
129
|
+
async function skillCatalog() {
|
|
130
|
+
const manifest = await loadManifest();
|
|
131
|
+
return Object.entries(manifest.skills).map(([name, entry]) => ({
|
|
132
|
+
name,
|
|
133
|
+
group: entry.group ?? null,
|
|
134
|
+
repoOnly: Boolean(entry.repoOnly)
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
function hasSelector(options) {
|
|
138
|
+
return Boolean(options.skill || options.group || options.all);
|
|
139
|
+
}
|
|
140
|
+
async function requireSelection(options, verb = "install") {
|
|
141
|
+
return selectSkills({ skill: options.skill, group: options.group, all: options.all }, await skillCatalog(), verb);
|
|
78
142
|
}
|
|
79
143
|
var agentLabels = { claude: "Claude Code", codex: "Codex", hermes: "Hermes" };
|
|
80
144
|
async function detectAgents() {
|
|
@@ -330,51 +394,69 @@ async function compare(destination, files) {
|
|
|
330
394
|
return { status: issues.length ? "drifted" : "verified", issues };
|
|
331
395
|
}
|
|
332
396
|
async function install(options) {
|
|
333
|
-
const
|
|
397
|
+
const skillNames = await requireSelection(options);
|
|
334
398
|
const choice = await prompt(options);
|
|
335
399
|
const base = baseFor(choice.mode, options.dir);
|
|
336
400
|
const agents = resolveAgents(choice.agent, choice.mode);
|
|
337
401
|
if (!agents.length)
|
|
338
402
|
return;
|
|
339
403
|
if (!options.dryRun)
|
|
340
|
-
return withInstallLock(base, () => installLocked(options,
|
|
341
|
-
return installLocked(options,
|
|
404
|
+
return withInstallLock(base, () => installLocked(options, skillNames, agents, base));
|
|
405
|
+
return installLocked(options, skillNames, agents, base, false);
|
|
342
406
|
}
|
|
343
|
-
async function installLocked(options,
|
|
407
|
+
async function installLocked(options, skillNames, agents, base, recover = true) {
|
|
344
408
|
if (recover)
|
|
345
409
|
await recoverInstall(base);
|
|
346
|
-
const
|
|
410
|
+
const sources = new Map;
|
|
411
|
+
for (const skillName of skillNames)
|
|
412
|
+
sources.set(skillName, await loadSource(skillName));
|
|
347
413
|
const operations = [];
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
414
|
+
const refusals = [];
|
|
415
|
+
for (const skillName of skillNames) {
|
|
416
|
+
const { files } = sources.get(skillName);
|
|
417
|
+
for (const agent of agents) {
|
|
418
|
+
const destination = join(base, surfaces[agent], skillName);
|
|
419
|
+
await assertNoSymlink(destination);
|
|
420
|
+
const parent = dirname(destination);
|
|
421
|
+
await assertNoSymlink(parent);
|
|
422
|
+
const existed = await exists(destination);
|
|
423
|
+
if (existed && !(await stat(destination)).isDirectory()) {
|
|
424
|
+
throw new Error(`Refusing to install over a non-directory: ${destination} — remove it and retry`);
|
|
359
425
|
}
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
426
|
+
if (existed) {
|
|
427
|
+
const comparison = await compare(destination, files);
|
|
428
|
+
if (comparison.status === "verified" && !options.force) {
|
|
429
|
+
console.log(`unchanged ${agent}: ${destination}`);
|
|
363
430
|
continue;
|
|
364
431
|
}
|
|
365
|
-
|
|
432
|
+
if (!options.force) {
|
|
433
|
+
if (options.dryRun) {
|
|
434
|
+
refusals.push({ agent, path: destination });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Refusing differing installation without --force: ${destination}`);
|
|
438
|
+
}
|
|
366
439
|
}
|
|
440
|
+
const suffix = randomUUID();
|
|
441
|
+
operations.push({ skill: skillName, agent, destination, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
|
|
367
442
|
}
|
|
368
|
-
const suffix = randomUUID();
|
|
369
|
-
operations.push({ skill: skillName, agent, destination, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
|
|
370
443
|
}
|
|
371
444
|
if (options.dryRun) {
|
|
372
445
|
for (const operation of operations)
|
|
373
446
|
console.log(`would install ${operation.agent}: ${operation.destination}`);
|
|
447
|
+
for (const destination of refusals)
|
|
448
|
+
console.log(`would replace ${destination.agent} (requires --force; installed copy differs): ${destination.path}`);
|
|
449
|
+
if (refusals.length && skillNames.length > 1) {
|
|
450
|
+
console.log(`this run would install nothing: ${refusals.length} destination(s) differ and --force was not given`);
|
|
451
|
+
process.exitCode = 1;
|
|
452
|
+
}
|
|
374
453
|
return;
|
|
375
454
|
}
|
|
376
|
-
if (!operations.length)
|
|
455
|
+
if (!operations.length) {
|
|
456
|
+
if (skillNames.length > 1)
|
|
457
|
+
console.log(`${skillNames.length} skills already installed and unchanged${options.group ? ` (${options.group})` : ""}`);
|
|
377
458
|
return;
|
|
459
|
+
}
|
|
378
460
|
const journalPath = join(base, ".vegastack", ".skills-install-transaction.json");
|
|
379
461
|
const staged = [];
|
|
380
462
|
const applied = [];
|
|
@@ -382,8 +464,9 @@ async function installLocked(options, skillName, agents, base, recover = true) {
|
|
|
382
464
|
for (const operation of operations) {
|
|
383
465
|
await mkdir(dirname(operation.destination), { recursive: true });
|
|
384
466
|
await assertNoSymlink(dirname(operation.destination), false);
|
|
467
|
+
const { source, files } = sources.get(operation.skill);
|
|
385
468
|
await cp(source, operation.stage, { recursive: true, dereference: false, errorOnExist: true });
|
|
386
|
-
await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, skill:
|
|
469
|
+
await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, skill: operation.skill, files }, null, 2)}
|
|
387
470
|
`, { flag: "wx" });
|
|
388
471
|
const stagedCheck = await compare(operation.stage, files);
|
|
389
472
|
if (stagedCheck.status !== "verified")
|
|
@@ -432,13 +515,21 @@ async function installLocked(options, skillName, agents, base, recover = true) {
|
|
|
432
515
|
await syncDirectory(dirname(journalPath));
|
|
433
516
|
for (const operation of applied)
|
|
434
517
|
console.log(`installed ${operation.agent}: ${operation.destination}`);
|
|
518
|
+
if (skillNames.length > 1) {
|
|
519
|
+
const skipped = options.all ? (await skillCatalog()).filter((entry) => entry.repoOnly).map((entry) => entry.name) : [];
|
|
520
|
+
const note = skipped.length ? ` (skipped ${skipped.length} repo-only: ${skipped.join(", ")} — name one explicitly to install it)` : "";
|
|
521
|
+
const installedCount = new Set(applied.map((operation) => operation.skill)).size;
|
|
522
|
+
const unchanged = skillNames.length - installedCount;
|
|
523
|
+
const unchangedNote = unchanged > 0 ? `, ${unchanged} already up to date` : "";
|
|
524
|
+
console.log(`installed ${installedCount} skills${options.group ? ` from ${options.group}` : ""}${unchangedNote}${note}`);
|
|
525
|
+
}
|
|
435
526
|
}
|
|
436
527
|
async function verify(options) {
|
|
437
528
|
const choice = await prompt(options);
|
|
438
529
|
const base = baseFor(choice.mode, options.dir);
|
|
439
530
|
const agents = resolveAgents(choice.agent, choice.mode);
|
|
440
|
-
const explicit =
|
|
441
|
-
const skills = explicit ?
|
|
531
|
+
const explicit = hasSelector(options);
|
|
532
|
+
const skills = explicit ? await requireSelection(options, "verify") : await bundledSkills();
|
|
442
533
|
let failed = false;
|
|
443
534
|
let found = 0;
|
|
444
535
|
for (const skillName of skills) {
|
|
@@ -485,48 +576,74 @@ async function latestPublishedVersion() {
|
|
|
485
576
|
}
|
|
486
577
|
}
|
|
487
578
|
async function removeSkill(options) {
|
|
488
|
-
const
|
|
579
|
+
const skillNames = await requireSelection(options, "remove");
|
|
489
580
|
const choice = await prompt(options);
|
|
490
581
|
const base = baseFor(choice.mode, options.dir);
|
|
491
582
|
const agents = resolveAgents(choice.agent, choice.mode);
|
|
492
583
|
if (!options.dryRun)
|
|
493
|
-
return withInstallLock(base, () => removeLocked(options,
|
|
494
|
-
return removeLocked(options,
|
|
495
|
-
}
|
|
496
|
-
async function removeLocked(options,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
584
|
+
return withInstallLock(base, () => removeLocked(options, skillNames, agents, base));
|
|
585
|
+
return removeLocked(options, skillNames, agents, base, false);
|
|
586
|
+
}
|
|
587
|
+
async function removeLocked(options, skillNames, agents, base, recover = true) {
|
|
588
|
+
if (recover)
|
|
589
|
+
await recoverInstall(base);
|
|
590
|
+
const targets = [];
|
|
591
|
+
for (const skill of skillNames) {
|
|
592
|
+
for (const agent of agents) {
|
|
593
|
+
const destination = join(base, surfaces[agent], skill);
|
|
594
|
+
if (!await exists(destination)) {
|
|
595
|
+
console.log(`not installed ${agent}: ${destination}`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
await assertNoSymlink(destination, false);
|
|
599
|
+
if (!options.force) {
|
|
600
|
+
const { files } = await loadSource(skill);
|
|
601
|
+
const comparison = await compare(destination, files);
|
|
602
|
+
if (comparison.status === "drifted")
|
|
603
|
+
throw new Error(`Installation differs from the bundled skill (possibly locally modified); re-run with --force to remove anyway: ${destination}`);
|
|
604
|
+
}
|
|
605
|
+
targets.push({ skill, agent, destination });
|
|
514
606
|
}
|
|
515
|
-
await rm(destination, { recursive: true, force: true });
|
|
516
|
-
removed += 1;
|
|
517
|
-
console.log(`removed ${agent}: ${destination}`);
|
|
518
607
|
}
|
|
519
|
-
if (
|
|
608
|
+
if (options.dryRun) {
|
|
609
|
+
for (const target of targets)
|
|
610
|
+
console.log(`would remove ${target.agent}: ${target.destination}`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
for (const target of targets) {
|
|
614
|
+
await rm(target.destination, { recursive: true, force: true });
|
|
615
|
+
console.log(`removed ${target.agent}: ${target.destination}`);
|
|
616
|
+
}
|
|
617
|
+
if (options.all) {
|
|
618
|
+
const skipped = (await skillCatalog()).filter((entry) => entry.repoOnly).map((entry) => entry.name);
|
|
619
|
+
if (skipped.length)
|
|
620
|
+
console.log(`left ${skipped.length} repo-only skills in place: ${skipped.join(", ")} — name one explicitly to remove it`);
|
|
621
|
+
}
|
|
622
|
+
if (!targets.length)
|
|
520
623
|
process.exitCode = 1;
|
|
521
624
|
}
|
|
522
625
|
async function list() {
|
|
523
626
|
const manifest = await loadManifest();
|
|
524
|
-
|
|
525
|
-
|
|
627
|
+
const entries = await skillCatalog();
|
|
628
|
+
const groups = [...new Set(entries.map((entry) => entry.group).filter((group) => group !== null))].sort();
|
|
629
|
+
const show = async (entry) => {
|
|
630
|
+
const skill = await readFile(join(bundleRoot, entry.name, "SKILL.md"), "utf8");
|
|
526
631
|
const description = skill.match(/^description: (.+)$/m)?.[1] ?? "";
|
|
527
|
-
const fileCount = Object.keys(manifest.skills[
|
|
528
|
-
console.log(
|
|
529
|
-
console.log(`
|
|
632
|
+
const fileCount = Object.keys(manifest.skills[entry.name]?.files ?? {}).length;
|
|
633
|
+
console.log(` ${entry.name} (${fileCount} files)${entry.repoOnly ? " [repo-only: not installed by --all]" : ""}`);
|
|
634
|
+
console.log(` ${description.length > 160 ? `${description.slice(0, 157)}...` : description}`);
|
|
635
|
+
};
|
|
636
|
+
for (const group of groups) {
|
|
637
|
+
console.log(`${group} — vegastack-skills add --group ${group}`);
|
|
638
|
+
for (const entry of entries.filter((item) => item.group === group).sort((a, b) => a.name.localeCompare(b.name)))
|
|
639
|
+
await show(entry);
|
|
640
|
+
console.log("");
|
|
641
|
+
}
|
|
642
|
+
const ungrouped = entries.filter((entry) => entry.group === null).sort((a, b) => a.name.localeCompare(b.name));
|
|
643
|
+
if (ungrouped.length) {
|
|
644
|
+
console.log("ungrouped");
|
|
645
|
+
for (const entry of ungrouped)
|
|
646
|
+
await show(entry);
|
|
530
647
|
}
|
|
531
648
|
}
|
|
532
649
|
async function doctor(options) {
|
package/package.json
CHANGED
|
@@ -37,7 +37,9 @@ Groups are this skill's responsibility: the repo shape, its `GROUP.md` files, an
|
|
|
37
37
|
2. **Put skills in it** — skillify's scaffolder places a new skill with `--group <name>`; it refuses — before writing anything — an unknown group, a malformed `GROUP.md`, a group with no README section, and a skill name already used at either depth, so a mistyped group never creates a stray family and the flat bundle never gets two skills of one name. Moving an existing skill into a group is a `git mv` plus its README row and its test's validator-import depth; `structure.mjs check` names anything left inconsistent.
|
|
38
38
|
3. **Check the shape** — `node packages/cli/scripts/structure.mjs check` blocks on illegal depth, name collisions, a missing or malformed `GROUP.md`, two groups sharing one `GROUP.md` title, a group with no README section, stray files in a group, a skill missing a contract meta file, packaging entries that disagree with the authored tree, a group-qualified packaging key, and README rows that are absent, duplicated, mispathed, or in the wrong section. It warns — without blocking, so warnings never fail `bun run check` — on an empty group, a group of one, and scaffolded placeholder text; `--strict` makes warnings exit 1. Dot-prefixed files and directories under `skills/` are ignored everywhere — discovery, this check, and the packaging build alike: they are tool and OS leftovers, not skills.
|
|
39
39
|
|
|
40
|
-
A group never reaches an
|
|
40
|
+
A group never reaches an installed **path**: the bundle is flat, so `GROUP.md` ships nowhere and an installed skill is always `<surface>/<bare-name>/`. A group is a *selection*, and the installer does expose it — `add`, `verify`, and `remove` each take one of a skill name, `--group <name>`, or `--all`, and a group install is one all-or-nothing transaction. Ungrouped skills at `skills/<name>/` stay fully supported; grouping is a choice, not a migration.
|
|
41
|
+
|
|
42
|
+
Two skills are marked repo-only in `packages/cli/repo-only.json`: `skill-maintainer` and `skillify` operate on this repository and do nothing useful elsewhere, so `--all` skips them while naming one explicitly still installs it. That list is data, validated by the build against the authored skills — never inferred from a skill's prose. Repo-only and group membership are independent: a group says where a skill is authored, the marker says who should install it.
|
|
41
43
|
|
|
42
44
|
## Workflow: scaffold a new skill
|
|
43
45
|
|
|
@@ -68,6 +68,8 @@ Source: https://hermes-agent.nousresearch.com/docs/user-guide/features/skills. <
|
|
|
68
68
|
|
|
69
69
|
Installer implication: the `@vegastack/skills` installer must treat Hermes as global-only — a "project install" for Hermes does not exist.
|
|
70
70
|
|
|
71
|
+
These paths are the harnesses' own discovery rules and are unaffected by how skills are selected. The installer's `--group` and `--all` flags choose *which* skills to act on; an installed skill is always `<surface>/<bare-name>/`, never `<surface>/<group>/<name>/`.
|
|
72
|
+
|
|
71
73
|
## Portability rules (this repo's policy)
|
|
72
74
|
|
|
73
75
|
One authored tree, three harnesses. Every skill in `skills/` follows all seven:
|
|
@@ -13,10 +13,10 @@ Instructions for the scheduled refresh agent (and any human running a manual ref
|
|
|
13
13
|
The deterministic runner is repo-shared, hosted at `tooling/refresh/`. Run from the repo root:
|
|
14
14
|
|
|
15
15
|
1. **Deterministic pass first** (no LLM judgment):
|
|
16
|
-
`node tooling/refresh/refresh-evidence.mjs --registry skills/skill-maintainer/refresh/sources.json`
|
|
16
|
+
`node tooling/refresh/refresh-evidence.mjs --registry skills/repo-tooling/skill-maintainer/refresh/sources.json`
|
|
17
17
|
drift/stale/unavailable results are the work-list. Exit 1 with a critical entry means fail-closed: the run must not be silently skipped. All four sources here are critical.
|
|
18
18
|
2. **Accept verified changes** in the same code path:
|
|
19
|
-
`node tooling/refresh/refresh-evidence.mjs --registry skills/skill-maintainer/refresh/sources.json --accept-baselines`
|
|
19
|
+
`node tooling/refresh/refresh-evidence.mjs --registry skills/repo-tooling/skill-maintainer/refresh/sources.json --accept-baselines`
|
|
20
20
|
This writes registry, cache, and drift report together — never hand-edit checksums, versions, or timestamps; they must always come from a run. Baselines are runner-seeded; when a new source is added or a verified change is accepted, this accept-baselines invocation is the only sanctioned way to update them.
|
|
21
21
|
3. **Semantic verification** for every source the deterministic pass flagged: read the changed page (fetch the registry URL), decide whether any `<!-- source: X -->` marked sentence in `references/standards.md` (or the mirrored SKILL.md hard-limits row) is now wrong, and propose the minimal edit. Unlike version-pin registries, checksum drift here is presumed meaningful until a human reads the diff — these pages define the standards themselves. Editorial churn may be accepted silently only after that read.
|
|
22
22
|
4. **One standing refresh PR**, branch `refresh/weekly`, force-updated on every run (never stacked duplicates; the weekly workflow .github/workflows/refresh.yml maintains it). PR body lists: each changed source, old→new checksum, links to the evidence, and which marked sentences changed and why. A maintainer review is mandatory before merge.
|
|
@@ -9,7 +9,7 @@ Instructions for the scheduled refresh agent (and any human running a manual ref
|
|
|
9
9
|
- the numeric limits and harness listing budgets table in `references/authoring.md` (marked `<!-- mirrored -->`);
|
|
10
10
|
- the model guidance section in `references/eval-playbook.md` (marked `<!-- volatile -->`).
|
|
11
11
|
|
|
12
|
-
Both mirror facts whose sources of truth are the agent-skills standards sources tracked in the **skill-maintainer** registry (`skills/skill-maintainer/refresh/sources.json`). `sources` here is therefore deliberately empty, and the refresh runner treats this registry as valid with nothing to fetch.
|
|
12
|
+
Both mirror facts whose sources of truth are the agent-skills standards sources tracked in the **skill-maintainer** registry (`skills/repo-tooling/skill-maintainer/refresh/sources.json`). `sources` here is therefore deliberately empty, and the refresh runner treats this registry as valid with nothing to fetch.
|
|
13
13
|
|
|
14
14
|
## How to refresh
|
|
15
15
|
|
|
@@ -222,14 +222,18 @@ export async function scaffoldSkill({ name, dir, group = null, write = false, no
|
|
|
222
222
|
// Every refusal belongs in this pre-flight. wireReadme runs after the tree is renamed into
|
|
223
223
|
// place and the packaging entry written, so anything discovered there would leave a half-wired
|
|
224
224
|
// skill on disk while reporting a refusal - or, worse, report success with no row at all.
|
|
225
|
+
// When a README exists, its row target must be resolvable BEFORE the tree is staged, so a
|
|
226
|
+
// refusal writes nothing. A wholly absent README or packaging.json keeps the documented
|
|
227
|
+
// degrade-to-`skipped:` contract (skillify's README, and the bare-repo test) - tightening that
|
|
228
|
+
// is a behaviour change of its own, tracked separately.
|
|
225
229
|
const readmePath = join(repoRoot, 'README.md')
|
|
226
230
|
if ((await entryAt(readmePath))?.isFile()) {
|
|
227
231
|
const lines = (await readFile(readmePath, 'utf8')).split('\n')
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
232
|
+
const rowTarget = findRowInsertion(lines, group, groupHeading)
|
|
233
|
+
if (rowTarget?.missingSection) {
|
|
230
234
|
throw new Error(`README.md has no "### ${groupHeading}" section for group "${group}" - create it with structure.mjs create-group`)
|
|
231
235
|
}
|
|
232
|
-
if (!
|
|
236
|
+
if (!rowTarget) {
|
|
233
237
|
throw new Error(`README.md has no ${group ? `table under "### ${groupHeading}"` : 'ungrouped Skills table'} to add a row to - every skill needs its row, so refusing rather than scaffolding a skill the structure check would block`)
|
|
234
238
|
}
|
|
235
239
|
}
|
|
@@ -237,6 +241,9 @@ export async function scaffoldSkill({ name, dir, group = null, write = false, no
|
|
|
237
241
|
// The generated test imports the repo validator by relative path, so its depth follows the
|
|
238
242
|
// skill's: skills/<name>/tests/ is three levels up, skills/<group>/<name>/tests/ is four.
|
|
239
243
|
const validatorPath = `${group ? '../../../..' : '../../..'}/packages/cli/scripts/validate-skill.mjs`
|
|
244
|
+
// Only a grouped skill gets the family-install line; an ungrouped one would otherwise ship a
|
|
245
|
+
// command naming a group that does not exist.
|
|
246
|
+
const groupInstallLine = group ? `\nnpx @vegastack/skills add --group ${group} # or the whole ${group} family` : ''
|
|
240
247
|
|
|
241
248
|
const outputs = templateFiles.map(([source, output]) => [source, output ?? `tests/${name}.test.ts`])
|
|
242
249
|
const plan = { name, group, target, files: outputs.map(([, output]) => output), wrote: false }
|
|
@@ -247,7 +254,11 @@ export async function scaffoldSkill({ name, dir, group = null, write = false, no
|
|
|
247
254
|
try {
|
|
248
255
|
for (const [source, output] of outputs) {
|
|
249
256
|
const body = await readFile(join(templatesRoot, source), 'utf8')
|
|
250
|
-
const rendered = body
|
|
257
|
+
const rendered = body
|
|
258
|
+
.replaceAll('{{name}}', name)
|
|
259
|
+
.replaceAll('{{date}}', date)
|
|
260
|
+
.replaceAll('{{validatorPath}}', validatorPath)
|
|
261
|
+
.replaceAll('{{groupInstallLine}}', groupInstallLine)
|
|
251
262
|
const destination = join(staging, output)
|
|
252
263
|
await mkdir(dirname(destination), { recursive: true })
|
|
253
264
|
await writeFile(destination, rendered)
|
package/skill-integrity.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"skills": {
|
|
4
4
|
"dev-architect": {
|
|
5
|
+
"group": "dev-skills",
|
|
6
|
+
"repoOnly": false,
|
|
5
7
|
"files": {
|
|
6
8
|
"SKILL.md": "74150dde2d3c83598769a4c9f8cd295a7496d9eb9a5e920a29cba8464eb58128",
|
|
7
9
|
"agents/openai.yaml": "99023b056d5c568ff3acc059e5bb7a4e6cfa5261cd0d35a525407f9e159de562",
|
|
@@ -20,6 +22,8 @@
|
|
|
20
22
|
}
|
|
21
23
|
},
|
|
22
24
|
"dev-chronicle": {
|
|
25
|
+
"group": "dev-skills",
|
|
26
|
+
"repoOnly": false,
|
|
23
27
|
"files": {
|
|
24
28
|
"SKILL.md": "23c6266758726d6435de38cb0cc142a94a0b55f2329f7ab6404e42fa5d0dee3a",
|
|
25
29
|
"agents/openai.yaml": "765b12e17b7fce88c53ca0fae735bf0efceb843cc3703b91bc1801ab5cac06b4",
|
|
@@ -29,6 +33,8 @@
|
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"dev-debug": {
|
|
36
|
+
"group": "dev-skills",
|
|
37
|
+
"repoOnly": false,
|
|
32
38
|
"files": {
|
|
33
39
|
"SKILL.md": "38ba9e67c00925f4e4738be333dc398218008df929fcf17b61398966ce4deee1",
|
|
34
40
|
"agents/openai.yaml": "4aa52b5c620b0bf4de7f1609867956ce762d83dc4297ab5670a68c206ae63075",
|
|
@@ -39,6 +45,8 @@
|
|
|
39
45
|
}
|
|
40
46
|
},
|
|
41
47
|
"dev-implement": {
|
|
48
|
+
"group": "dev-skills",
|
|
49
|
+
"repoOnly": false,
|
|
42
50
|
"files": {
|
|
43
51
|
"SKILL.md": "724f6bc83467c28dc61a45161d3ffba7fecef2712eff3333bdf40d762731db44",
|
|
44
52
|
"agents/openai.yaml": "1abab383deca54d32749fd9156f9a6211fe96e02e0166dc8a16ff8459ac31b74",
|
|
@@ -52,6 +60,8 @@
|
|
|
52
60
|
}
|
|
53
61
|
},
|
|
54
62
|
"dev-intake": {
|
|
63
|
+
"group": "dev-skills",
|
|
64
|
+
"repoOnly": false,
|
|
55
65
|
"files": {
|
|
56
66
|
"SKILL.md": "4754383297822d3f6e0ad8a391ac76499d83c74f424b90b928199d6c2649dc71",
|
|
57
67
|
"agents/openai.yaml": "5473d2d3efffc518c07dfa8e6788c6535a5054ceca696119c5856ad265ef5581",
|
|
@@ -63,6 +73,8 @@
|
|
|
63
73
|
}
|
|
64
74
|
},
|
|
65
75
|
"dev-plan": {
|
|
76
|
+
"group": "dev-skills",
|
|
77
|
+
"repoOnly": false,
|
|
66
78
|
"files": {
|
|
67
79
|
"SKILL.md": "eb2ce1542b1769fa6e7d9dc7be9fc9c84df8b353ded16e39e1e578ee372c1aca",
|
|
68
80
|
"agents/openai.yaml": "4b9ccc9870b4eafb73c1bc4492a0b5a530097bbcbae11dbf3235160f4f05587b",
|
|
@@ -74,6 +86,8 @@
|
|
|
74
86
|
}
|
|
75
87
|
},
|
|
76
88
|
"dev-review": {
|
|
89
|
+
"group": "dev-skills",
|
|
90
|
+
"repoOnly": false,
|
|
77
91
|
"files": {
|
|
78
92
|
"SKILL.md": "be32aa737cfd3309f7413407470ebffeedf6284f0f98cef6ae185c478c896c26",
|
|
79
93
|
"agents/openai.yaml": "3f35d3dc60f179d4ac2526997c4d10173ba3018d06f0dbe77e209ce2978fa702",
|
|
@@ -87,6 +101,8 @@
|
|
|
87
101
|
}
|
|
88
102
|
},
|
|
89
103
|
"dev-setup": {
|
|
104
|
+
"group": "dev-skills",
|
|
105
|
+
"repoOnly": false,
|
|
90
106
|
"files": {
|
|
91
107
|
"SKILL.md": "d8eb7741983998f56ff8aff52f66ec3887b7fd055c26cc232e1ff41da6fab7c1",
|
|
92
108
|
"agents/openai.yaml": "be790c04fdf910e71642cfadf1a7c07c217bf7fad18581610feb4c31a79651f2",
|
|
@@ -100,6 +116,8 @@
|
|
|
100
116
|
}
|
|
101
117
|
},
|
|
102
118
|
"dev-ship": {
|
|
119
|
+
"group": "dev-skills",
|
|
120
|
+
"repoOnly": false,
|
|
103
121
|
"files": {
|
|
104
122
|
"SKILL.md": "aa3bd8b0a1ac076309190f2008896f24ebe1216352872ff428c1b88b55c06e65",
|
|
105
123
|
"agents/openai.yaml": "8f18cfd505131557eb4c9f7b7f54e9069f787be4c162403fc30253eed49a06ea",
|
|
@@ -111,6 +129,8 @@
|
|
|
111
129
|
}
|
|
112
130
|
},
|
|
113
131
|
"dev-status": {
|
|
132
|
+
"group": "dev-skills",
|
|
133
|
+
"repoOnly": false,
|
|
114
134
|
"files": {
|
|
115
135
|
"SKILL.md": "56415c2f69bb1a2fcb488b9846a20e632eb902b05c5b2b58ae9206d3eaffd77b",
|
|
116
136
|
"agents/openai.yaml": "72435947f5afef0c8a2c170e987b14fe6057b174318c16b6b538ae8d129791fb",
|
|
@@ -121,20 +141,24 @@
|
|
|
121
141
|
}
|
|
122
142
|
},
|
|
123
143
|
"skill-maintainer": {
|
|
144
|
+
"group": "repo-tooling",
|
|
145
|
+
"repoOnly": true,
|
|
124
146
|
"files": {
|
|
125
|
-
"SKILL.md": "
|
|
147
|
+
"SKILL.md": "1733d2f1fa756cbb37c74ecee0d48ddd9abf31155c4dd72eba10af31c33d88ec",
|
|
126
148
|
"agents/openai.yaml": "0b89d4e6416cf20b448f5322f3d7433cfde333d2b4a05b42ed53db3759701fb4",
|
|
127
149
|
"references/release-ops.md": "b5df1922b34daf7751c332c019a0ea770a48283aaa2e25f83c9851bef47999b1",
|
|
128
|
-
"references/standards.md": "
|
|
129
|
-
"refresh/REFRESH.md": "
|
|
150
|
+
"references/standards.md": "0dd7fcb6e2f0dd3aae7167227d7ec085481bc629aa027af35d48e5d0f7b9df86",
|
|
151
|
+
"refresh/REFRESH.md": "d3ba44be2c264b849d8a815111c049e782720428d5d0c5cea23998b05e50d58f",
|
|
130
152
|
"refresh/sources.json": "4977152381d53555196ac676f95e61b4eaa301309a17560da5cda722010eb80f"
|
|
131
153
|
}
|
|
132
154
|
},
|
|
133
155
|
"skillify": {
|
|
156
|
+
"group": "repo-tooling",
|
|
157
|
+
"repoOnly": true,
|
|
134
158
|
"files": {
|
|
135
159
|
"SKILL.md": "310ac32ff4d557cc166a470fd1866060dfc5f03898e8ff3da9c406b84580742b",
|
|
136
160
|
"agents/openai.yaml": "df320cf32a8db06c213f7892844dab810b7e96cfbf32502d25185d4a4167ef87",
|
|
137
|
-
"assets/templates/README.md.template": "
|
|
161
|
+
"assets/templates/README.md.template": "49dccb4e9b5bdb07c254f9142502b3fd11fd5b083d8661b0ae6a567dec79de74",
|
|
138
162
|
"assets/templates/REFRESH.md.template": "ff86afade565d668333e7f6d0432213760fd68a62b8ba22897a8601b74c94ac0",
|
|
139
163
|
"assets/templates/SKILL.md.template": "b038775f97b13b54227fe41a767a48cdaa922a93d1732f22c2d1d79bff52ca00",
|
|
140
164
|
"assets/templates/openai.yaml.template": "2fbf488369825109d6cad4f17e7b729a40d9e3c43c7f01bc5dfa16361d59c086",
|
|
@@ -143,9 +167,9 @@
|
|
|
143
167
|
"assets/templates/trigger-queries.json.template": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
|
|
144
168
|
"references/authoring.md": "de61bc54140e07bb1910d5adac4e988e10fea9bd11b3e27234c0c699aa7bbf0d",
|
|
145
169
|
"references/eval-playbook.md": "1f463dfea8d69b7022712776794369892643aa020186fe3a3707c493a6c2d4e9",
|
|
146
|
-
"refresh/REFRESH.md": "
|
|
170
|
+
"refresh/REFRESH.md": "695f3283b27c14accf4095137898ce1f6dc40749483c5a5705e80aa9693f08c2",
|
|
147
171
|
"refresh/sources.json": "e554e80c72c16dc70587361804db9e0cf2716c128820b2612d5983a009b2b242",
|
|
148
|
-
"scripts/scaffold-skill.mjs": "
|
|
172
|
+
"scripts/scaffold-skill.mjs": "8bc3314f800fcff0bf5f797fdd82424ceb16a45ac03c4ee0eebcc416dfc41b97"
|
|
149
173
|
}
|
|
150
174
|
}
|
|
151
175
|
}
|