@vegastack/skills 0.13.0 → 0.16.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/dev-implement/SKILL.md +1 -0
- package/skill/dev-review/SKILL.md +9 -2
- package/skill/dev-review/references/dispatch-prompts.md +12 -0
- package/skill/dev-review/references/security-axis.md +13 -1
- package/skill/dev-review/scripts/skill-scan.mjs +774 -0
- package/skill/dev-setup/SKILL.md +3 -0
- package/skill/dev-setup/assets/dev-profile.md.template +1 -0
- package/skill/skill-maintainer/SKILL.md +3 -1
- package/skill/skill-maintainer/references/standards.md +44 -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 +38 -13
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
|
@@ -47,6 +47,7 @@ A changeset entry's first line is **one plain sentence** naming the change in be
|
|
|
47
47
|
Before claiming ANY status: **identify** the command that proves it → **run** it fresh and complete → **read** the full output and exit code → only then claim, with the evidence. Tests pass ⇒ a fresh run with 0 failures — never "should pass", never a previous run. Build succeeds ⇒ exit 0. Bug fixed ⇒ the original symptom re-tested. A subagent finished ⇒ you inspected its diff or report file — never its say-so.
|
|
48
48
|
|
|
49
49
|
- Run what dev.md's `tests:` knob requires; a `risky` issue gets focused security, failure, and recovery checks on top. When dev.md has a `## Verify` runbook, run the app and smoke-check the flows it names. Verify is pre-merge only; post-release checks live in `## Ship` and belong to dev-ship.
|
|
50
|
+
- Run the skill-scan guard here, unconditionally: `node <path-to-dev-review>/scripts/skill-scan.mjs --json` — it reads dev.md's `skill-scan:` knob itself and exits 0 saying it skipped when the project authors no skills, so there is no condition to remember. Exit 2 blocks the hand-back: fix the finding, or take it to the operator for a justified baseline rule — never widen a suppression to get past it. Its findings below the blocking bar are the security axis's to triage at review.
|
|
50
51
|
- UI changed and `ui-evidence: playwright` → capture screenshots of the key states and upload to the shared evidence repo (dev.md `evidence-repo`) under `<this-repo-name>/<issue-number>/<timestamp>-<name>.png` via the contents API, never a clone: `base64 < <file> | tr -d '\n' | gh api -X PUT repos/<evidence-repo>/contents/<path> -f message="evidence #<issue>" -F content=@-` (piped stdin so large screenshots never hit argv limits; timestamped names avoid collisions; a 409 from a concurrent upload just means retry). Link them in the evidence comment — links, never embeds (private-repo images don't render inline). Evidence repo unreachable → name local paths and say so; the hand-back never blocks on it.
|
|
51
52
|
- dev.md's Ship or Verify section is an empty TODO next to visible machinery → finish normally, then suggest re-running dev-setup.
|
|
52
53
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev-review
|
|
3
|
-
description: Independent review of finished implementation work — a diff against its brief and plan. Use when dev-implement's review step runs, when asked to "review this branch/diff/issue", "give this a second pair of eyes", "check the finished work on issue N", when a cross-agent session (Claude or Codex) is handed a REVIEW REQUEST,
|
|
3
|
+
description: Independent review of finished implementation work — a diff against its brief and plan — and the skill-scan vulnerability guard. Use when dev-implement's review step runs, when asked to "review this branch/diff/issue", "give this a second pair of eyes", "check the finished work on issue N", when a cross-agent session (Claude or Codex) is handed a REVIEW REQUEST, when review findings need a fix loop, re-review, or adjudication, or when asked to scan skills for vulnerabilities, triage scanner findings, or judge whether a third-party skill is safe to install. Not for reviewing an unbuilt plan (dev-plan's approval gate), architecture review (dev-architect), shipping gates (dev-ship), or generic PR review in repos outside this workflow.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dev-review
|
|
@@ -13,13 +13,15 @@ Nearest neighbors: `dev-implement` invokes this per dev.md's `review:` knob and
|
|
|
13
13
|
|
|
14
14
|
Build the review package first: `git log --oneline <base>..<head>` + `git diff --stat` + `git diff -U10`, written to `.vegastack/.tmp/<issue>-<slug>/review-<base7>..<head7>.diff`. Reviewers get paths — the brief (issue body), the plan comment, the package file, the project's `.vegastack/review-known-patterns.md` — plus the binding constraints copied verbatim. Reviewers write their full reports to `.tmp` files and return short status; a dead reviewer's findings survive on disk.
|
|
15
15
|
|
|
16
|
+
When dev.md names a `skill-scan:` root, the security dispatch also gets the scan report: `node <path-to-this-skill>/scripts/skill-scan.mjs --json > .vegastack/.tmp/<issue>-<slug>/skill-scan.json` (add `--llm` for the semantic pass — advisory only, never a gate; it is non-deterministic and a degraded run inflates scores). The same guard runs at `dev-implement`'s Verify gate, so by review time it has already passed; the axis is here to triage what sits below the blocking bar and to judge whether anything above it was suppressed rather than fixed.
|
|
17
|
+
|
|
16
18
|
## The axes — parallel, fresh, never merged
|
|
17
19
|
|
|
18
20
|
| Axis | Runs | Judges |
|
|
19
21
|
|---|---|---|
|
|
20
22
|
| **Spec** | always | the diff vs the CURRENT brief + plan: missing, scope creep, implemented-but-wrong — quoting the brief line per finding; includes the tests-are-real rubric |
|
|
21
23
|
| **Standards** | always | project rules (known-patterns file + repo docs, which override) + the fixed smell baseline pasted in full into its prompt |
|
|
22
|
-
| **Security** | on `risky`,
|
|
24
|
+
| **Security** | on `risky`, when touch points hit auth, money, user data, or external input, **or when the diff touches a skill under dev.md's `skill-scan:` root** | data-flow traces, exploitability before severity, and triage of the skill scan's findings — method in [security-axis](references/security-axis.md) |
|
|
23
25
|
|
|
24
26
|
Each axis is a fresh subagent with no memory of writing the code (its prompt: [dispatch-prompts](references/dispatch-prompts.md)). Axes report separately and are never re-ranked into one list — a change can pass one axis and fail another, and merging lets one mask the other.
|
|
25
27
|
|
|
@@ -59,6 +61,11 @@ Severities: `[CRITICAL]` (security axis: exploitable now — blocks) > `[MUST-FI
|
|
|
59
61
|
|
|
60
62
|
- Default quiet profile: spec, bugs, and security always; style only where a documented rule exists. The comment count is the noise metric.
|
|
61
63
|
- `.vegastack/review-known-patterns.md` (seed: [template](assets/review-known-patterns.md.template)) holds the project's never-flag patterns — each entry REQUIRES a **"Still flag if:"** exception clause; a suppression without one is a blind spot. Operator dismissals of findings get appended there by dev-implement's corrections loop, so a dismissed pattern stays dismissed.
|
|
64
|
+
- The skill scan's suppressions follow the same discipline in its own baseline file, and the guard enforces the clause rather than trusting it — a rule scoped `id:` with no `path:` is a repo-wide blind spot. A finding suppressed rather than fixed is a review finding, not a settled matter.
|
|
65
|
+
|
|
66
|
+
## A scan with no issue attached
|
|
67
|
+
|
|
68
|
+
A scan run outside an issue — a standalone check, or the pre-publish guard in dev.md's `## Ship` — has no review comment to land in, and never gets attached to an unrelated issue. Findings there route to `dev-intake`: offer the operator one `risky` issue whose **brief body** carries the findings, their locations, and what is already known about each cause. That is intake's job, with its questions, its scope call, and its approval — not a comment posted somewhere convenient.
|
|
62
69
|
|
|
63
70
|
## Cross-agent — the independence upgrade
|
|
64
71
|
|
|
@@ -88,6 +88,18 @@ violations of documented standards may be [MUST-FIX]; baseline smells are
|
|
|
88
88
|
|
|
89
89
|
Compose the security dispatch from [security-axis](security-axis.md): the shared preamble above, then that file's Method steps, finding format (the three extra lines), severity definitions, and standing red lines, quoted into the prompt — the reviewer must receive them in full, not a pointer it cannot follow.
|
|
90
90
|
|
|
91
|
+
Where a scanner report exists, add its path and that file's "Scanner evidence" rules to the same dispatch:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Scanner report: <path to skill-scan.json or the project's equivalent>.
|
|
95
|
+
Treat every entry as a CANDIDATE finding, never a verdict: read the source at
|
|
96
|
+
its file:line, trace the flow, and assign severity yourself by exploitability.
|
|
97
|
+
The report's aggregate score is context, not a ranking. Entries the baseline
|
|
98
|
+
suppressed are in scope — judge whether each rule is scoped as narrowly as its
|
|
99
|
+
cause and whether its stated re-trigger condition would actually fire. Say so
|
|
100
|
+
in your verdict line if the report says the scan did not complete.
|
|
101
|
+
```
|
|
102
|
+
|
|
91
103
|
## Re-review brief (scoped, every fix round)
|
|
92
104
|
|
|
93
105
|
```
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# The security axis
|
|
2
2
|
|
|
3
|
-
Runs on `risky` issues,
|
|
3
|
+
Runs on `risky` issues, whenever the diff's touch points hit an auth surface, money, user data, or externally-controlled input, and whenever the diff touches a skill under dev.md's `skill-scan:` root — the trigger is the surface, not the label alone.
|
|
4
|
+
|
|
5
|
+
## Scanner evidence — a finding's start, never its end
|
|
6
|
+
|
|
7
|
+
Where the project runs a scanner (this family ships `scripts/skill-scan.mjs` for agent skills; a project may name others), its report is an **input to this axis**, on the same footing as the diff. It is evidence, not a verdict:
|
|
8
|
+
|
|
9
|
+
- **A scanner hit is a candidate finding.** It arrives with a rule ID and a `file:line` and nothing else — no data flow, no attacker, no exploitability. Promote it to a finding only after the Method below fills those in; a hit you cannot trace goes to the collapsed low-confidence block like any other hunch.
|
|
10
|
+
- **Read the source at the location before judging it.** The scanner matched text; whether that text is a vulnerability is your call, made against the file, not the summary.
|
|
11
|
+
- **Severity is yours, not the scanner's.** Map its finding onto this file's ladder by exploitability. A scanner HIGH that cannot be exploited is not `[CRITICAL]`; a scanner MEDIUM with a traced path to a real sink can be.
|
|
12
|
+
- **Never downgrade an unexplained HIGH or CRITICAL** on reputation, score, or "it's our own code". Either the trace shows why it does not hold, or it stands.
|
|
13
|
+
- **The aggregate risk score is not a finding.** It is distorted upward by documentation of the very mechanics being scanned and downward by unrelated suppressions. Quote it for context; never rank on it.
|
|
14
|
+
- **Suppressions are in scope for this axis.** A finding silenced by a baseline rule rather than fixed is reviewable: check the rule is scoped as narrowly as its cause and that its stated re-trigger condition is one that would actually fire.
|
|
15
|
+
- **A degraded or partial scan is not a clean scan.** If the report says the run did not complete, say so in the verdict line rather than reporting its numbers.
|
|
4
16
|
|
|
5
17
|
## Method — evidence before severity
|
|
6
18
|
|