@vegastack/skills 0.16.1 → 0.17.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 CHANGED
@@ -2,17 +2,25 @@
2
2
 
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
+ Install the whole dev workflow, once per machine:
6
+
7
+ ```sh
8
+ npx @vegastack/skills add --group dev-skills --global
9
+ ```
10
+
11
+ `--global` is the recommended install: the skills land in your home directory and are available in every project you open. Drop it for a project-local install when a repository should carry its own copy.
12
+
13
+ See what else is bundled:
14
+
5
15
  ```sh
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
16
+ npx @vegastack/skills list
9
17
  ```
10
18
 
11
19
  ## Skills in this package
12
20
 
13
21
  ### `dev-skills` — the issue-driven dev workflow
14
22
 
15
- Install the family with `add --group dev-skills`.
23
+ Install the family with `add --group dev-skills --global`.
16
24
 
17
25
  | Skill | What it does |
18
26
  |---|---|
@@ -58,13 +66,46 @@ These operate on the vegastack-skills repository itself and do nothing useful in
58
66
 
59
67
  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
68
 
69
+ The ten dev-workflow skills:
70
+
71
+ ```sh
72
+ npx @vegastack/skills add --group dev-skills --global
73
+ ```
74
+
75
+ Everything worth installing outside this repo:
76
+
77
+ ```sh
78
+ npx @vegastack/skills add --all --global
79
+ ```
80
+
81
+ Check the family against the manifest:
82
+
61
83
  ```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
84
+ npx @vegastack/skills verify --group dev-skills --global
66
85
  ```
67
86
 
87
+ Uninstall it again:
88
+
89
+ ```sh
90
+ npx @vegastack/skills remove --group dev-skills --global
91
+ ```
92
+
93
+ ## Upgrading and health checks
94
+
95
+ Upgrade to the latest release. `--force` is required because `add` refuses to overwrite an installed copy that differs from the bundle rather than silently discarding local edits:
96
+
97
+ ```sh
98
+ npx @vegastack/skills@latest add --group dev-skills --global --force
99
+ ```
100
+
101
+ Diagnose an install — integrity across all skills, plus installed-vs-latest version:
102
+
103
+ ```sh
104
+ npx @vegastack/skills doctor --global
105
+ ```
106
+
107
+ Run `doctor` without `--global` from inside a project to additionally check that project's `.vegastack/dev.md` profile; the global run skips that check, since the profile is per-project by design.
108
+
68
109
  ## Flags
69
110
 
70
111
  | Flag | Meaning |
@@ -82,18 +123,22 @@ npx @vegastack/skills remove --group dev-skills # uninstall it again
82
123
 
83
124
  `--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.
84
125
 
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).
126
+ 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.
86
127
 
87
128
  ## Agent surfaces
88
129
 
89
- | Agent | Project install | Global install |
130
+ `--global` is the recommended install and the only one that can cover all three runtimes at once. `--project` is the flag default, so pass `--global` explicitly.
131
+
132
+ | Agent | Global install (recommended) | Project install |
90
133
  |---|---|---|
91
- | Claude Code | `.claude/skills/` | `~/.claude/skills/` |
92
- | Codex | `.agents/skills/` | `~/.agents/skills/` |
93
- | Hermes | — (Hermes discovers skills globally only) | `~/.hermes/skills/` |
134
+ | Claude Code | `~/.claude/skills/` | `.claude/skills/` |
135
+ | Codex | `~/.agents/skills/` | `.agents/skills/` |
136
+ | Hermes | `~/.hermes/skills/` | — (Hermes discovers skills globally only) |
94
137
 
95
138
  `--agent hermes` therefore requires `--global`; `--agent all` on a project install covers codex+claude and prints a notice about hermes.
96
139
 
140
+ Prefer a project install when a repository should carry its own copy — so collaborators get the same skills from a checkout, or so one project can pin a version while the rest of the machine moves on. Pick one or the other per skill rather than both: in Claude Code a personal (global) skill takes precedence over a project one, so a project-local copy would not override a global install of the same skill.
141
+
97
142
  ## Integrity model
98
143
 
99
144
  The package ships a checksum manifest that is verified at install and by `verify` — it proves the installed bytes match what was packed, not who published it. Publisher identity is attested separately by npm provenance, generated by the trusted-publishing release pipeline. Verify it with `npm audit signatures` or on the package's npm page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegastack/skills",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Installer and verifier for VegaStack Agent Skills (Claude Code, Codex, and Hermes)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,8 @@ 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
+ The guard provisions its own scanner: it locates the SkillSpector CLI through whatever channel installed it (uv, brew, pipx) and runs it by absolute path, so a working install is never reported as missing because `PATH` differs between the operator's shell and the agent's. dev.md's `skillspector-update:` knob decides the rest — `auto` (the default) installs it when absent and upgrades it before each scan, falling back to the installed copy on any failure; `notify` only reports what upstream published; `off` never touches the network. `--no-provision` forces one run to leave the machine alone. An upgrade that changes anything is reported before the findings, because after an upgrade a new finding is the tool having learned something, not the diff having broken something.
17
+
16
18
  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
19
 
18
20
  ## The axes — parallel, fresh, never merged
@@ -1,3 +1,18 @@
1
1
  # Refresh contract — dev-review
2
2
 
3
- Evergreen: this skill asserts no version pins, numeric vendor limits, or dated claims — its content is review discipline (axes, severities, the bounded loop, dispatch briefs, the smell baseline, cross-agent handoff shape), all versionless. The `codex exec` / `claude -p` invocation forms are deliberately treated as durable CLI surfaces; dev-setup's detection covers their presence per machine. Revisit if a future edit introduces a volatile fact.
3
+ Most of this skill is versionless review discipline (axes, severities, the bounded loop, dispatch briefs, the smell baseline, cross-agent handoff) and asserts nothing that can go stale. The `codex exec` / `claude -p` invocation forms stay deliberately untracked as durable CLI surfaces; dev-setup's detection covers their presence per machine.
4
+
5
+ **The evergreen waiver was retired on 01-09-2026** (issue #83). `scripts/lib/skillspector.mjs` now parses the output of third-party commands and calls a third-party API, so the skill carries volatile facts for the first time — and the waiver's own escape clause said to revisit exactly then.
6
+
7
+ What the registry tracks, and why each one is load-bearing rather than decorative: every parser below fails **silently** if its upstream surface changes — returning "not installed", which the guard reports as a missing scanner instead of a parsing bug. That silence is the whole reason these are registered.
8
+
9
+ | Source | The claim it holds up | Breaks how |
10
+ |---|---|---|
11
+ | `SS-INSTALL` | `uv tool install git+https://github.com/NVIDIA/skillspector.git` is upstream's documented install, and the package is not on PyPI | the install command installs nothing, or the wrong thing |
12
+ | `SS-VERSION-OUTPUT` | `skillspector --version` prints `SkillSpector v<x.y.z>` on **stdout**, warnings on stderr | version reporting goes null; the baseline-pin warning stops firing |
13
+ | `SS-RELEASES-API` | the GitHub releases endpoint shape, and its 60-request/hour unauthenticated limit | `notify` silently reports nothing |
14
+ | `UV-TOOL-LIST` | `uv tool list --show-paths` prints `- skillspector (/abs/path)` | uv-installed scanners read as absent |
15
+ | `BREW-PREFIX` | `brew list --versions` exits non-zero when absent, while `brew --prefix` exits **0 with a path for any known formula** whether or not it is installed | brew detection returns a path that does not exist |
16
+ | `PIPX-LIST` | `pipx list --short` exits 0 even with nothing installed, so its output must be parsed | pipx-installed scanners read as absent, or absence reads as present |
17
+
18
+ Drift in any of them means reading the changed surface and updating both the parser and its dated comment in the same reviewed PR — never auto-applying. The parsers each have a unit test pinning the exact output shape, so a corrected parser has a failing test to satisfy.
@@ -1,6 +1,133 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "retrievalBaseline": "2026-08-28",
4
- "note": "Evergreen waiver recorded in REFRESH.md; sources deliberately empty.",
5
- "sources": []
3
+ "policy": {
4
+ "defaultChecksumScope": "html-text-v1",
5
+ "offline": "Use cached metadata only; fail closed when an entry is missing or older than thresholdDays.",
6
+ "drift": "Read the changed surface, then update the parser AND its dated comment in one reviewed PR; never auto-apply. Each parser has a unit test pinning the output shape.",
7
+ "copyright": "Store claim metadata, URLs, hashes and concise excerpts only; never archive third-party documentation.",
8
+ "cadence": "weekly scheduled refresh; thresholdDays must be >= 14 (2x cadence) so one missed run never breaches a threshold"
9
+ },
10
+ "sources": [
11
+ {
12
+ "id": "SS-INSTALL",
13
+ "service": "SkillSpector install instructions (uv; not on PyPI)",
14
+ "kind": "official-docs",
15
+ "stability": "vendor-docs",
16
+ "thresholdDays": 14,
17
+ "critical": true,
18
+ "urls": {
19
+ "primary": "https://github.com/NVIDIA/SkillSpector"
20
+ },
21
+ "versionDetection": {
22
+ "type": "manual-review"
23
+ },
24
+ "topics": [
25
+ "skillspector-install"
26
+ ],
27
+ "affected": [
28
+ "scripts/lib/skillspector.mjs",
29
+ "README.md"
30
+ ]
31
+ },
32
+ {
33
+ "id": "SS-VERSION-OUTPUT",
34
+ "service": "skillspector --version output shape (stdout vs stderr)",
35
+ "kind": "official-docs",
36
+ "stability": "vendor-docs",
37
+ "thresholdDays": 14,
38
+ "critical": true,
39
+ "urls": {
40
+ "primary": "https://github.com/NVIDIA/SkillSpector"
41
+ },
42
+ "versionDetection": {
43
+ "type": "manual-review"
44
+ },
45
+ "topics": [
46
+ "skillspector-version"
47
+ ],
48
+ "affected": [
49
+ "scripts/lib/skillspector.mjs"
50
+ ]
51
+ },
52
+ {
53
+ "id": "SS-RELEASES-API",
54
+ "service": "GitHub releases endpoint shape and unauthenticated rate limit",
55
+ "kind": "official-docs",
56
+ "stability": "vendor-docs",
57
+ "thresholdDays": 14,
58
+ "critical": false,
59
+ "urls": {
60
+ "primary": "https://docs.github.com/en/rest/releases/releases"
61
+ },
62
+ "versionDetection": {
63
+ "type": "manual-review"
64
+ },
65
+ "topics": [
66
+ "skillspector-releases"
67
+ ],
68
+ "affected": [
69
+ "scripts/lib/skillspector.mjs"
70
+ ]
71
+ },
72
+ {
73
+ "id": "UV-TOOL-LIST",
74
+ "service": "uv tool list --show-paths output shape",
75
+ "kind": "official-docs",
76
+ "stability": "vendor-docs",
77
+ "thresholdDays": 14,
78
+ "critical": true,
79
+ "urls": {
80
+ "primary": "https://docs.astral.sh/uv/concepts/tools/"
81
+ },
82
+ "versionDetection": {
83
+ "type": "manual-review"
84
+ },
85
+ "topics": [
86
+ "channel-uv"
87
+ ],
88
+ "affected": [
89
+ "scripts/lib/skillspector.mjs"
90
+ ]
91
+ },
92
+ {
93
+ "id": "BREW-PREFIX",
94
+ "service": "brew list --versions vs brew --prefix exit semantics",
95
+ "kind": "official-docs",
96
+ "stability": "vendor-docs",
97
+ "thresholdDays": 14,
98
+ "critical": false,
99
+ "urls": {
100
+ "primary": "https://docs.brew.sh/Manpage"
101
+ },
102
+ "versionDetection": {
103
+ "type": "manual-review"
104
+ },
105
+ "topics": [
106
+ "channel-brew"
107
+ ],
108
+ "affected": [
109
+ "scripts/lib/skillspector.mjs"
110
+ ]
111
+ },
112
+ {
113
+ "id": "PIPX-LIST",
114
+ "service": "pipx list --short exit code and output",
115
+ "kind": "official-docs",
116
+ "stability": "vendor-docs",
117
+ "thresholdDays": 14,
118
+ "critical": false,
119
+ "urls": {
120
+ "primary": "https://pipx.pypa.io/stable/docs/"
121
+ },
122
+ "versionDetection": {
123
+ "type": "manual-review"
124
+ },
125
+ "topics": [
126
+ "channel-pipx"
127
+ ],
128
+ "affected": [
129
+ "scripts/lib/skillspector.mjs"
130
+ ]
131
+ }
132
+ ]
6
133
  }
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ // Everything about the SkillSpector CLI itself — where it is, installing it,
3
+ // upgrading it, reading its version. `skill-scan.mjs` keeps owning the scan and
4
+ // the verdict and calls in here once, before scanning.
5
+ //
6
+ // Named for the tool, not for its role: this repo's own machinery is "skill
7
+ // scan", and the third-party binary is always "skillspector" by its exact name,
8
+ // so the generic word stays free (operator's rule, 01-09-2026).
9
+ //
10
+ // Every command runs through an injected `run`, and every path check through an
11
+ // injected `exists`, so unit tests never install software or touch the disk.
12
+ // Self-contained (ships with dev-review; no cross-skill imports, no dependencies).
13
+ import { execFileSync } from 'node:child_process';
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ // Upstream's own documented quick install, verified 01-09-2026 against the
18
+ // SkillSpector README, which documents uv and no other package manager. The git
19
+ // URL is not optional: the package is NOT published to PyPI (HTTP 404), so a
20
+ // bare `pip install skillspector` can never work.
21
+ export const INSTALL_COMMAND = ['uv', ['tool', 'install', 'git+https://github.com/NVIDIA/skillspector.git']];
22
+
23
+ // Per channel, in probe order. `detect` proves the tool is actually installed;
24
+ // `locate` turns that into an absolute executable path.
25
+ const UPGRADE = {
26
+ uv: ['uv', ['tool', 'upgrade', 'skillspector']],
27
+ brew: ['brew', ['upgrade', 'skillspector']],
28
+ pipx: ['pipx', ['upgrade', 'skillspector']],
29
+ };
30
+
31
+ // A command runner that never throws: a non-zero exit is data, not an
32
+ // exception, because "brew is not installed here" is an ordinary answer to
33
+ // "where is skillspector". stderr is folded into stdout so a failure message
34
+ // survives for the report.
35
+ export function defaultRun(cmd, args, { timeoutMs = 300_000 } = {}) {
36
+ try {
37
+ const stdout = execFileSync(cmd, args, {
38
+ encoding: 'utf8',
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ // `env` is passed explicitly, as skill-scan.mjs does: under Bun a mutated
41
+ // process.env is NOT inherited by execFileSync children.
42
+ env: { ...process.env },
43
+ timeout: timeoutMs,
44
+ maxBuffer: 16 * 1024 * 1024,
45
+ });
46
+ return { ok: true, stdout };
47
+ } catch (error) {
48
+ const out = `${error.stdout?.toString() ?? ''}${error.stderr?.toString() ?? ''}`.trim();
49
+ return { ok: false, stdout: out || error.message || '' };
50
+ }
51
+ }
52
+
53
+ // `uv tool list --show-paths` prints one line per tool and one indented line per
54
+ // executable it installed:
55
+ // skillspector v2.11.0 (/home/x/.local/share/uv/tools/skillspector)
56
+ // - skillspector (/home/x/.local/bin/skillspector)
57
+ // The executable line is the one that matters — the first is the venv, which is
58
+ // not runnable. Anchored on both sides so `skillspector-extra` cannot match.
59
+ export function parseUvToolList(text) {
60
+ for (const line of String(text ?? '').split('\n')) {
61
+ const match = /^-\s+skillspector\s+\((.+)\)\s*$/.exec(line.trim());
62
+ if (match) return match[1];
63
+ }
64
+ return null;
65
+ }
66
+
67
+ // `pipx list --short` exits 0 whether or not anything is installed (verified
68
+ // 01-09-2026 — it prints "nothing has been installed with pipx" and succeeds),
69
+ // so the exit code proves nothing and the listing has to be read.
70
+ export function parsePipxList(text) {
71
+ return String(text ?? '')
72
+ .split('\n')
73
+ .some((line) => /^skillspector(\s|$)/.test(line.trim()));
74
+ }
75
+
76
+ // Ask each channel where it put the executable, in order, and believe only a
77
+ // path that is actually on disk.
78
+ //
79
+ // The existence check is not defensive padding: `brew --prefix <formula>` exits
80
+ // 0 and prints a path for any formula it KNOWS, installed or not — verified
81
+ // 01-09-2026, where it named /opt/homebrew/opt/skillspector while nothing was
82
+ // installed there. Detection therefore runs `brew list --versions` first, and
83
+ // the check below is the backstop for every channel.
84
+ export function locateSkillspector({ run = defaultRun, exists = existsSync } = {}) {
85
+ const believe = (channel, path) => (path && exists(path) ? { channel, path } : null);
86
+
87
+ const uv = run('uv', ['tool', 'list', '--show-paths']);
88
+ if (uv.ok) {
89
+ const found = believe('uv', parseUvToolList(uv.stdout));
90
+ if (found) return found;
91
+ }
92
+
93
+ if (run('brew', ['list', '--versions', 'skillspector']).ok) {
94
+ const prefix = run('brew', ['--prefix', 'skillspector']);
95
+ if (prefix.ok && prefix.stdout.trim()) {
96
+ const found = believe('brew', join(prefix.stdout.trim(), 'bin', 'skillspector'));
97
+ if (found) return found;
98
+ }
99
+ }
100
+
101
+ const pipx = run('pipx', ['list', '--short']);
102
+ if (pipx.ok && parsePipxList(pipx.stdout)) {
103
+ const dir = run('pipx', ['environment', '--value', 'PIPX_BIN_DIR']);
104
+ if (dir.ok && dir.stdout.trim()) {
105
+ const found = believe('pipx', join(dir.stdout.trim(), 'skillspector'));
106
+ if (found) return found;
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+
114
+ // Strip C0/C1 controls (ANSI escapes included) from anything a command printed:
115
+ // this text reaches a terminal report, and package-manager output carries names
116
+ // from outside the repo. Mirrors skill-scan.mjs's own `safe()`.
117
+ function safe(text) {
118
+ // eslint-disable-next-line no-control-regex
119
+ return String(text).replace(/[\u0000-\u001f\u007f-\u009f]/g, '?');
120
+ }
121
+
122
+ // `skillspector --version` prints "SkillSpector v2.11.0" on STDOUT while its
123
+ // missing-API-key warnings go to stderr (verified 01-09-2026). `run` hands back
124
+ // stdout alone on success, so the warnings can never contaminate the match.
125
+ export function readVersion({ path, run = defaultRun }) {
126
+ const result = run(path, ['--version']);
127
+ if (!result.ok) return null;
128
+ const match = /SkillSpector\s+v?(\d+\.\d+\.\d+\S*)/i.exec(result.stdout);
129
+ return match ? match[1] : null;
130
+ }
131
+
132
+ // The lines a package manager reported moving. uv prints one `+ pkg==x` /
133
+ // `- pkg==y` line per dependency it changed; anything else simply yields none.
134
+ function changedLines(stdout) {
135
+ return String(stdout ?? '')
136
+ .split('\n')
137
+ .map((line) => safe(line.trim()))
138
+ .filter((line) => /^[+-]\s*\S/.test(line));
139
+ }
140
+
141
+ // Install when absent, upgrade when present — and never throw: a machine
142
+ // without a network, without uv, or with a locked package manager must fall
143
+ // back to whatever is already installed and let the scan proceed. Only a
144
+ // SkillSpector that cannot be found at all blocks, and that is skill-scan's
145
+ // call, not this function's.
146
+ //
147
+ // No version check runs first, deliberately. `uv tool upgrade` moves the whole
148
+ // dependency tree while the version string can hold steady (verified
149
+ // 01-09-2026: langsmith 0.11.2 -> 0.12.0 under an unchanged v2.11.0), so
150
+ // comparing versions would report "current" about a tool that just changed.
151
+ export function provisionSkillspector({ mode, located, run = defaultRun }) {
152
+ const idle = { action: 'none', before: null, after: null, changed: [], message: '' };
153
+
154
+ // `notify` reports; it never touches the machine. The release lookup that
155
+ // makes it useful belongs to the caller, which owns the network policy.
156
+ if (mode !== 'auto') return located ? { ...idle, before: readVersion({ path: located.path, run }) } : idle;
157
+
158
+ if (!located) {
159
+ const [cmd, args] = INSTALL_COMMAND;
160
+ const result = run(cmd, args);
161
+ if (!result.ok) {
162
+ return { ...idle, action: 'failed', message: safe(result.stdout), changed: changedLines(result.stdout) };
163
+ }
164
+ // `after` stays null: reading it needs the path, and only a fresh locate
165
+ // knows where the install landed. The caller re-locates and fills it in.
166
+ return { action: 'installed', before: null, after: null, changed: changedLines(result.stdout), message: '' };
167
+ }
168
+
169
+ const upgrade = UPGRADE[located.channel];
170
+ if (!upgrade) {
171
+ return { ...idle, message: `no upgrade command is known for the ${safe(String(located.channel))} channel` };
172
+ }
173
+
174
+ const before = readVersion({ path: located.path, run });
175
+ const result = run(upgrade[0], upgrade[1]);
176
+ if (!result.ok) {
177
+ return { action: 'failed', before, after: before, changed: [], message: safe(result.stdout) };
178
+ }
179
+ return {
180
+ action: 'upgraded',
181
+ before,
182
+ after: readVersion({ path: located.path, run }),
183
+ changed: changedLines(result.stdout),
184
+ message: '',
185
+ };
186
+ }
187
+
188
+
189
+ // Upstream's releases feed. Unauthenticated and rate-limited to 60 requests an
190
+ // hour per IP (verified 01-09-2026), which `notify` stays far inside because it
191
+ // asks once per guard run and only in that mode.
192
+ export const RELEASES_URL = 'https://api.github.com/repos/NVIDIA/SkillSpector/releases/latest';
193
+
194
+ async function fetchReleaseJson(url) {
195
+ // GitHub rejects requests without a User-Agent.
196
+ const response = await fetch(url, {
197
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'vegastack-skill-scan' },
198
+ signal: AbortSignal.timeout(10_000),
199
+ });
200
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
201
+ return response.json();
202
+ }
203
+
204
+ // The newest published release, or null. EVERY failure path is null and never a
205
+ // throw: `notify` is a courtesy line in a report, and a guard must not turn a
206
+ // flaky network into a verdict. A tag that is not a plain version (a nightly, a
207
+ // moved pointer) is rejected rather than reported as a version.
208
+ export async function latestRelease({ fetchJson = fetchReleaseJson, url = RELEASES_URL } = {}) {
209
+ let body;
210
+ try {
211
+ body = await fetchJson(url);
212
+ } catch {
213
+ return null;
214
+ }
215
+ if (!body || typeof body !== 'object') return null;
216
+ const tag = typeof body.tag_name === 'string' ? body.tag_name.trim() : '';
217
+ const match = /^v?(\d+\.\d+\.\d+\S*)$/.exec(tag);
218
+ return match ? match[1] : null;
219
+ }
220
+
221
+ export const UPGRADE_COMMANDS = UPGRADE;
@@ -15,6 +15,13 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync } from 'node:fs';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { basename, join, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
+ import {
19
+ defaultRun,
20
+ latestRelease,
21
+ locateSkillspector,
22
+ provisionSkillspector,
23
+ readVersion,
24
+ } from './lib/skillspector.mjs';
18
25
 
19
26
  // The clause every suppression must carry, mirroring the "Still flag if:"
20
27
  // requirement on .vegastack/review-known-patterns.md entries: a suppression
@@ -173,7 +180,7 @@ export function parseBaseline(text) {
173
180
  coverage.push({ skill: raw.skill, file: raw.file, sha256: raw.sha256, reason: raw.reason });
174
181
  });
175
182
 
176
- return { rules, fingerprints: rawFingerprints, coverage, errors, warns };
183
+ return { rules, fingerprints: rawFingerprints, coverage, errors, warns, scannerVersion: data.scanner_version ?? null };
177
184
  }
178
185
 
179
186
  // Absolute paths, sorted, of the skill directories under `root`. A directory is
@@ -318,13 +325,34 @@ export function evaluateScan(facts) {
318
325
  baselineErrors = [],
319
326
  skills = [],
320
327
  scanErrors = [],
328
+ skillspector = {},
329
+ baselinePin = {},
321
330
  } = facts;
322
331
 
323
332
  // Environment failures first: when the scanner never ran, a finding list is
324
333
  // not evidence of anything, and the real cause must read before the noise.
325
334
  if (binaryMissing) {
326
- blocks.push(`the \`skillspector\` binary is not on PATH — ${INSTALL_HINT}, or set skill-scan: none if this project has no skills`);
335
+ blocks.push(
336
+ `the \`skillspector\` binary could not be found — no install channel (uv, brew, pipx) reports it and it is not on PATH — ${INSTALL_HINT}; or, if it runs through a wrapper or container, point VSK_SKILLSPECTOR at that executable; or set skill-scan: none if this project has no skills`,
337
+ );
338
+ }
339
+ // An update that could not happen is a note, never a block: the scan ran on
340
+ // the copy that was already installed, which is exactly the documented
341
+ // fallback. Sanitized — this text comes from a package manager.
342
+ if (skillspector.action === 'failed' && skillspector.message) {
343
+ warns.push(`skillspector update failed, scanned with the installed copy instead — ${safe(skillspector.message)}`);
344
+ }
345
+ // A fingerprint is a content hash tied to the scanner that produced it, so a
346
+ // version change can stop it matching and quietly un-suppress its finding.
347
+ // Warn, never block: the finding coming back IS the loud outcome, and the pin
348
+ // is never moved automatically — that would assert a suppression still holds
349
+ // for a scanner nobody has run.
350
+ if (baselinePin.fingerprints > 0 && baselinePin.scannerVersion && skillspector.version && baselinePin.scannerVersion !== skillspector.version) {
351
+ warns.push(
352
+ `baseline pins scanner_version ${safe(baselinePin.scannerVersion)} for ${baselinePin.fingerprints} fingerprint(s) but skillspector ${safe(skillspector.version)} ran — re-verify those suppressions and move the pin deliberately, never automatically`,
353
+ );
327
354
  }
355
+
328
356
  if (rootMissing) {
329
357
  blocks.push(`scan root "${rootMissing}" does not exist — build it first if it is a build output, or correct dev.md's skill-scan: knob`);
330
358
  }
@@ -502,6 +530,29 @@ export function scanRootDeclarations(devMdText) {
502
530
  return [...String(devMdText ?? '').matchAll(KNOB_LINE)].map((match) => match[1]);
503
531
  }
504
532
 
533
+ // The sibling knob governing SkillSpector itself: off | notify | auto, absent
534
+ // reading as `auto` so an existing profile inherits the default without an
535
+ // edit. Same tolerant layout matching and same conflict discipline as
536
+ // `skill-scan:` — a knob the guard cannot see reads as absent, and absent must
537
+ // not silently mean something different from what the author wrote.
538
+ //
539
+ // Named for the tool, not for our guard: `skill-scan:` is our machinery,
540
+ // `skillspector-update:` is the third-party binary (operator's rule, 01-09-2026).
541
+ const UPDATE_KNOB_LINE = /^[ \t]*(?:[-*+][ \t]+)?skillspector-update:[ \t]*(\S+)/gm;
542
+
543
+ export const UPDATE_MODES = new Set(['off', 'notify', 'auto']);
544
+
545
+ // Every declared value, unvalidated. The caller refuses a conflict or an
546
+ // unrecognised value rather than picking one: guessing which mode the author
547
+ // meant is exactly the judgement a guard must not make.
548
+ export function updateModeDeclarations(devMdText) {
549
+ return [...String(devMdText ?? '').matchAll(UPDATE_KNOB_LINE)].map((match) => match[1]);
550
+ }
551
+
552
+ export function resolveUpdateMode(devMdText) {
553
+ return updateModeDeclarations(devMdText)[0] ?? 'auto';
554
+ }
555
+
505
556
  export function resolveScanRoot(devMdText) {
506
557
  const value = scanRootDeclarations(devMdText)[0];
507
558
  if (!value || value === 'none') return null;
@@ -529,10 +580,16 @@ function normalizeIssue(raw) {
529
580
  // Impure: shells out to the scanner, once per skill. `--baseline` is rejected
530
581
  // together with `--recursive` ("scan each sub-skill with its own baseline"), so
531
582
  // the loop is the supported path, not an optimization we passed up.
532
- export function gatherFacts({ root, baselinePath, llm }) {
583
+ export function gatherFacts({ root, baselinePath, llm, binary: binaryOverride }) {
533
584
  // VSK_SKILLSPECTOR is a TEST SEAM (stubs the scanner in unit tests), mirroring
534
- // ship-gate.mjs's VSK_GH. Normal runs resolve `skillspector` from PATH.
535
- const binary = process.env.VSK_SKILLSPECTOR || 'skillspector';
585
+ // ship-gate.mjs's VSK_GH. `binaryOverride` is the absolute path the CLI
586
+ // resolved through the tool's own install channel; a bare PATH lookup is the
587
+ // last resort, not the first.
588
+ //
589
+ // Locating deliberately happens in the CLI and NOT here: gatherFacts is
590
+ // driven directly by unit tests, and probing uv/brew/pipx from inside it
591
+ // would make the suite shell out to whatever is installed on the machine.
592
+ const binary = process.env.VSK_SKILLSPECTOR || binaryOverride || 'skillspector';
536
593
  const base = {
537
594
  binaryMissing: false,
538
595
  rootMissing: null,
@@ -551,6 +608,10 @@ export function gatherFacts({ root, baselinePath, llm }) {
551
608
  base.baselineErrors = parsed.errors;
552
609
  base.baselineWarns = parsed.warns;
553
610
  base.coverageAccepted = parsed.coverage;
611
+ // Only fingerprints are version-coupled: they are content hashes the
612
+ // scanner computed, so a different scanner may stop matching them. Rules
613
+ // bind to id+path and survive an upgrade untouched.
614
+ base.baselinePin = { scannerVersion: parsed.scannerVersion, fingerprints: parsed.fingerprints.length };
554
615
  }
555
616
  // Short-circuit: with a bad baseline nothing the scan reports is trustworthy —
556
617
  // suppressions may not apply — and the scanner would reject the file once per
@@ -685,6 +746,62 @@ export function gatherFacts({ root, baselinePath, llm }) {
685
746
  return base;
686
747
  }
687
748
 
749
+ // The provisioning sequence, extracted so it is testable: the CLI passes the
750
+ // real commands, unit tests pass fakes. Returns the `skillspector` report block.
751
+ //
752
+ // `mode` is a MACHINE policy, never a per-scan-root one — see the CLI, which
753
+ // reads it from the profile even when --root chose what to scan.
754
+ export async function provisionForRun({
755
+ mode,
756
+ locate,
757
+ provision,
758
+ versionOf,
759
+ pathVisible,
760
+ fetchLatest,
761
+ }) {
762
+ const state = {
763
+ mode,
764
+ channel: null,
765
+ path: null,
766
+ version: null,
767
+ action: 'none',
768
+ before: null,
769
+ after: null,
770
+ changed: [],
771
+ message: '',
772
+ available: null,
773
+ resolvedOutsidePath: false,
774
+ };
775
+
776
+ let located = locate();
777
+ const result = provision({ mode, located });
778
+ state.action = result.action;
779
+ state.changed = result.changed;
780
+ state.message = result.message;
781
+ state.before = result.before;
782
+ state.after = result.after;
783
+ // An install lands somewhere only a fresh probe knows about.
784
+ if (result.action === 'installed') located = locate();
785
+
786
+ if (located) {
787
+ state.channel = located.channel;
788
+ state.path = located.path;
789
+ state.version = versionOf(located.path);
790
+ // A fresh install has no "before", so its "after" is simply the version now
791
+ // installed — otherwise the report says "(unchanged)" about a tool that was
792
+ // not there a moment ago.
793
+ if (state.action === 'installed') state.after = state.version;
794
+ // Located through its channel while a bare PATH lookup cannot see it. This
795
+ // is reported, never warned about: it is the ordinary case this feature
796
+ // exists to serve, and making it a warn would pin the exit code at 1
797
+ // forever for exactly the setup that motivated the work.
798
+ state.resolvedOutsidePath = !pathVisible();
799
+ }
800
+
801
+ if (mode === 'notify') state.available = await fetchLatest();
802
+ return state;
803
+ }
804
+
688
805
  const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
689
806
  if (invokedDirectly) {
690
807
  const argv = process.argv.slice(2);
@@ -693,6 +810,8 @@ if (invokedDirectly) {
693
810
  return index === -1 ? undefined : argv[index + 1];
694
811
  };
695
812
  const json = argv.includes('--json');
813
+ // Forces this one run to leave the machine untouched, whatever the knob says.
814
+ const noProvision = argv.includes('--no-provision');
696
815
  const devMdPath = get('--dev-md') || '.vegastack/dev.md';
697
816
 
698
817
  let root = get('--root');
@@ -701,6 +820,50 @@ if (invokedDirectly) {
701
820
  let outcome = { blocks: [], warns: [] };
702
821
  let facts = { skills: [] };
703
822
  let baselinePath = get('--baseline') ?? null;
823
+ let updateMode = 'auto';
824
+ let binary;
825
+ // Warns raised before the scan runs. `outcome` is REASSIGNED by evaluateScan,
826
+ // so anything pushed onto it beforehand would be silently discarded.
827
+ const preWarns = [];
828
+ const skillspector = {
829
+ mode: 'auto',
830
+ channel: null,
831
+ path: null,
832
+ version: null,
833
+ action: 'none',
834
+ before: null,
835
+ after: null,
836
+ changed: [],
837
+ message: '',
838
+ available: null,
839
+ resolvedOutsidePath: false,
840
+ };
841
+
842
+ // The update mode is read from the profile ALWAYS, including for --root runs.
843
+ // --root chooses what to scan; it never decides whether this machine may be
844
+ // written to. Skipping this is how `skillspector-update: off` got ignored on
845
+ // exactly the invocation dev-review documents for vetting a stranger's skill.
846
+ {
847
+ let profileForMode = null;
848
+ try {
849
+ profileForMode = readFileSync(devMdPath, 'utf8');
850
+ } catch {
851
+ // An explicit --root may legitimately run outside any project.
852
+ }
853
+ if (profileForMode !== null) {
854
+ const declared = [...new Set(updateModeDeclarations(profileForMode))];
855
+ const unusable = declared.length > 1 || declared.some((value) => !UPDATE_MODES.has(value));
856
+ // A profile we cannot read unambiguously must not authorise writing to
857
+ // the machine. The non---root path below turns the same conditions into
858
+ // blocks; here the run continues, but touching nothing.
859
+ updateMode = unusable ? 'off' : resolveUpdateMode(profileForMode);
860
+ if (unusable && explicitRoot) {
861
+ preWarns.push(
862
+ `${devMdPath} does not give skillspector-update a single recognised value — this run left the machine untouched`,
863
+ );
864
+ }
865
+ }
866
+ }
704
867
 
705
868
  if (!explicitRoot) {
706
869
  // "Could not read the profile" and "the profile says none" are different
@@ -720,6 +883,22 @@ if (invokedDirectly) {
720
883
  `${devMdPath} gives skill-scan conflicting values (${declared.join(', ')}) — an example line above the real knob would otherwise silently decide the gate; leave exactly one`,
721
884
  );
722
885
  }
886
+ const updateDeclared = [...new Set(updateModeDeclarations(devMd))];
887
+ if (updateDeclared.length > 1) {
888
+ outcome.blocks.push(
889
+ `${devMdPath} gives skillspector-update conflicting values (${updateDeclared.join(', ')}) — leave exactly one`,
890
+ );
891
+ }
892
+ const unknown = updateDeclared.filter((value) => !UPDATE_MODES.has(value));
893
+ if (unknown.length > 0) {
894
+ // Defaulting an unrecognised value to `auto` would silently install
895
+ // software because of a typo. The guard refuses instead.
896
+ outcome.blocks.push(
897
+ `${devMdPath} sets skillspector-update to ${unknown.join(', ')} — expected one of off, notify, auto`,
898
+ );
899
+ }
900
+ updateMode = resolveUpdateMode(devMd);
901
+
723
902
  root = resolveScanRoot(devMd);
724
903
  skipped = root === null && declared.length <= 1;
725
904
  // The project's own suppressions apply to the project's own skills. They
@@ -731,23 +910,47 @@ if (invokedDirectly) {
731
910
  }
732
911
 
733
912
  if (!skipped && outcome.blocks.length === 0) {
913
+ skillspector.mode = noProvision ? 'off' : updateMode;
914
+
915
+ // VSK_SKILLSPECTOR is the test seam, and it means "this exact binary" — so
916
+ // it suppresses locating AND provisioning. Without that, every CLI test
917
+ // would shell out to whatever uv/brew happen to hold on the machine running
918
+ // the suite, and a unit suite that installs software is not a unit suite.
919
+ if (!process.env.VSK_SKILLSPECTOR) {
920
+ Object.assign(
921
+ skillspector,
922
+ await provisionForRun({
923
+ mode: skillspector.mode,
924
+ locate: () => locateSkillspector(),
925
+ provision: ({ mode, located }) => provisionSkillspector({ mode, located }),
926
+ versionOf: (path) => readVersion({ path }),
927
+ pathVisible: () => defaultRun('skillspector', ['--version']).ok,
928
+ fetchLatest: () => latestRelease(),
929
+ }),
930
+ );
931
+ binary = skillspector.path ?? undefined;
932
+ }
933
+
734
934
  // An uncaught throw would leave node exiting 1 — which in this guard's own
735
935
  // scheme reads as "pass with warnings". A crash is not a pass.
736
936
  try {
737
- facts = gatherFacts({ root, baselinePath, llm: argv.includes('--llm') });
937
+ facts = gatherFacts({ root, baselinePath, llm: argv.includes('--llm'), binary });
938
+ facts.skillspector = skillspector;
738
939
  outcome = evaluateScan(facts);
739
940
  } catch (error) {
740
- facts = { skills: [] };
941
+ facts = { skills: [], skillspector };
741
942
  outcome = { blocks: [`the scan failed unexpectedly: ${error.message}`], warns: [] };
742
943
  }
743
944
  }
744
945
 
946
+ outcome = { ...outcome, warns: [...preWarns, ...outcome.warns] };
745
947
  const ok = outcome.blocks.length === 0;
746
948
  if (json) {
747
949
  console.log(JSON.stringify({
748
950
  guard: 'skill-scan',
749
951
  ok,
750
952
  skipped,
953
+ skillspector,
751
954
  ...outcome,
752
955
  // The full normalized issue list, not a count: dev-review's Security axis
753
956
  // is told to read the source at each finding's file:line and to judge
@@ -763,6 +966,23 @@ if (invokedDirectly) {
763
966
  console.log('skill-scan: BLOCKED');
764
967
  for (const b of outcome.blocks) console.log(` block: ${b}`);
765
968
  } else {
969
+ // The version/dependency change reads BEFORE the findings: after an
970
+ // upgrade, new findings are the tool having learned something, not the diff
971
+ // having broken something, and an operator who cannot see that debugs the
972
+ // wrong thing.
973
+ if (skillspector.action === 'installed' || skillspector.action === 'upgraded') {
974
+ const span = skillspector.before === skillspector.after
975
+ ? `version ${skillspector.version ?? 'unknown'} (unchanged)`
976
+ : `version ${skillspector.before ?? 'none'} → ${skillspector.after ?? skillspector.version ?? 'unknown'}`;
977
+ console.log(`skill-scan: skillspector ${skillspector.action} via ${skillspector.channel ?? 'uv'} — ${span}`);
978
+ for (const line of skillspector.changed) console.log(` ${line}`);
979
+ }
980
+ if (skillspector.action === 'failed') {
981
+ console.log(`skill-scan: skillspector update failed, continuing with the installed copy — ${skillspector.message}`);
982
+ }
983
+ if (skillspector.available && skillspector.available !== skillspector.version) {
984
+ console.log(`skill-scan: skillspector ${skillspector.available} is available (installed: ${skillspector.version ?? 'unknown'})`);
985
+ }
766
986
  console.log(`skill-scan: ${ok ? (outcome.warns.length ? 'pass with warnings' : 'pass') : 'BLOCKED'}`);
767
987
  for (const entry of facts.skills) {
768
988
  console.log(` ${entry.name}: score ${entry.score} ${entry.severity} — ${entry.issues.length} finding(s)`);
@@ -27,7 +27,7 @@ Facts are your job; decisions are the user's. Gather these silently and present
27
27
  | native issue types | `gh api orgs/<org>/issue-types` — an `Epic` type routes parents to it; absent endpoint or type → the `epic` label fallback ([conventions](references/conventions.md)) |
28
28
  | Codex CLI (cross-agent review) | `command -v codex` — absent → record the gap in dev.md `## Environments` and recommend installing it |
29
29
  | agent skills in the repo | a directory holding skill folders, flat or one group deep — each folder carrying its own entry point — drafts the `skill-scan:` knob at that path; none found drafts `none`. Declare it **once**: a second `skill-scan:` line with a different value, even in a prose example, makes the profile ambiguous and the guard refuses. Where the project builds a flattened bundle the knob names the **built** directory, because unpackaged test fixtures are deliberately adversarial and score higher than anything that ships |
30
- | SkillSpector (skill scanning) | `command -v skillspector`absent while `skill-scan:` is set record it in `## Environments` with `uv tool install git+https://github.com/NVIDIA/skillspector.git`; the guard refuses rather than skipping, so this is a real gap |
30
+ | SkillSpector (skill scanning) | nothing to detectdev-review's guard locates the CLI through whatever channel holds it (uv, brew, pipx) and, under `skillspector-update: auto`, installs it when absent and upgrades it before each scan. Confirm the drafted `skillspector-update:` value with the operator instead: `auto` is the default and provisions silently, `notify` only reports what upstream published, `off` never touches the network |
31
31
 
32
32
  Not a git repo, or no origin remote → this is a **greenfield run, not an error**: follow the greenfield playbook in [stack-playbooks](references/stack-playbooks.md) — interview for the intended stack, offer `git init` and `gh repo create` each on its own yes, and render dev.md from the chosen playbook's conventions with TODO lines where machinery doesn't exist yet. A declined remote skips labels and records the TODO plainly.
33
33
 
@@ -49,7 +49,7 @@ Ask with your harness's question tool — AskUserQuestion in Claude Code, `reque
49
49
  - Playbook matched → show the drafted `## Ship` runbook (each step `auto:`, `ask:`, or `guard:` — every `guard:` line carries its runnable command inline), the `changelog:` convention, and the `release:` knob (per-merge or on-request) for confirmation; a keep-a-changelog convention with no CHANGELOG.md yet → offer to seed the skeleton; no machinery → "Ship: merge only" and move on
50
50
  - Guards drafted → offer to write their CI backstop steps into the project's workflow files (the local `guard:` lines run without CI); each file on the user's yes — release guards only, never general CI
51
51
  - Environments or run commands detected → confirm the drafted `## Environments` and `## Verify` bullets
52
- - Agent skills detected → confirm the drafted `skill-scan:` root, the `## Verify` bullet running the guard (preceded by the build command when the root is a build output), and a blocking `guard:` line in `## Ship` before the publish step — the published artifact is what the world installs. Skills detected but the operator declines the scan → `skill-scan: none`, said plainly, not silently omitted
52
+ - Agent skills detected → confirm the drafted `skill-scan:` root, the `skillspector-update:` value (`auto` by default — say plainly that it installs and upgrades the SkillSpector CLI on this machine without asking again, and that `notify` or `off` opt out), the `## Verify` bullet running the guard (preceded by the build command when the root is a build output), and a blocking `guard:` line in `## Ship` before the publish step — the published artifact is what the world installs. Skills detected but the operator declines the scan → `skill-scan: none`, said plainly, not silently omitted
53
53
  - Evidence repo (`ui-evidence: playwright`) → default is the owner's **shared** `<owner>/dev-review-evidence`; if it doesn't exist, offer `gh repo create <owner>/dev-review-evidence --private --add-readme` + the layout/retention README — created once, every project points at it. An org naming policy that rejects the name → pick the closest compliant name with the user and record it in the knob (the name is a knob value, not a contract)
54
54
  - App architecture detected → confirm the drafted `## Architecture` (hosting, stage, and kind are what detection usually can't fill — ask those); nothing detected → delete the section, the `stack:` line is enough
55
55
  - A legacy `.vegastack/arch.md` exists → fold its knob lines into `## Architecture`, offer each dated `notes:` line to the decision register on the user's yes, then offer to delete arch.md
@@ -15,6 +15,7 @@ evidence-repo: {{owner}}/dev-review-evidence # shared across projects; this pr
15
15
  gates: 3 # 3 = approve/PR/merge · 2 = approve + one "ship it" · 1 = direct-to-main, ship word merges locally and pushes, no PR
16
16
  tests: required # required | logic-only
17
17
  skill-scan: none # ONE line only — directory holding the agent skills to scan (the BUILT one where a bundle is generated) | none. dev-review's guard reads this and refuses if a second line gives a different value
18
+ skillspector-update: auto # off | notify | auto — how the SkillSpector CLI itself is kept current. auto installs it when absent and upgrades it before each scan, falling back to the installed copy on any failure; notify only reports what upstream has published; off never touches the network. ONE line only, same as above
18
19
  merge: squash
19
20
  branch: <type>/<issue>-<slug> # type: feat | fix | docs | chore | refactor — the only place this list lives
20
21
  labels: needs-operator needs-plan ready working for-operator risky research quick-build full-plan epic # epic label marks map parents only where the org has no native Epic issue type
@@ -5,8 +5,10 @@ TODO: one-paragraph summary for humans and agents browsing the repo. The agent e
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- npx @vegastack/skills add {{name}}{{groupInstallLine}}
8
+ npx @vegastack/skills add {{name}} --global
9
9
  ```
10
+ {{groupInstallBlock}}
11
+ `--global` installs into your home directory, where the skill is available in every project; drop it for a project-local install.
10
12
 
11
13
  ## What's in this skill
12
14
 
@@ -247,9 +247,12 @@ export async function scaffoldSkill({ name, dir, group = null, write = false, no
247
247
  // The generated test imports the repo validator by relative path, so its depth follows the
248
248
  // skill's: skills/<name>/tests/ is three levels up, skills/<group>/<name>/tests/ is four.
249
249
  const validatorPath = `${group ? '../../../..' : '../../..'}/packages/cli/scripts/validate-skill.mjs`
250
- // Only a grouped skill gets the family-install line; an ungrouped one would otherwise ship a
251
- // command naming a group that does not exist.
252
- const groupInstallLine = group ? `\nnpx @vegastack/skills add --group ${group} # or the whole ${group} family` : ''
250
+ // Only a grouped skill gets the family-install block; an ungrouped one would otherwise ship a
251
+ // command naming a group that does not exist. It is its own fence, not a second line in the
252
+ // first one: pasting a shared fence would run the alternative too.
253
+ const groupInstallBlock = group
254
+ ? `\nOr the whole ${group} family at once:\n\n\`\`\`sh\nnpx @vegastack/skills add --group ${group} --global\n\`\`\`\n`
255
+ : ''
253
256
 
254
257
  const outputs = templateFiles.map(([source, output]) => [source, output ?? `tests/${name}.test.ts`])
255
258
  const plan = { name, group, target, files: outputs.map(([, output]) => output), wrote: false }
@@ -264,7 +267,7 @@ export async function scaffoldSkill({ name, dir, group = null, write = false, no
264
267
  .replaceAll('{{name}}', name)
265
268
  .replaceAll('{{date}}', date)
266
269
  .replaceAll('{{validatorPath}}', validatorPath)
267
- .replaceAll('{{groupInstallLine}}', groupInstallLine)
270
+ .replaceAll('{{groupInstallBlock}}', groupInstallBlock)
268
271
  const destination = join(staging, output)
269
272
  await mkdir(dirname(destination), { recursive: true })
270
273
  await writeFile(destination, rendered)
@@ -89,26 +89,27 @@
89
89
  "group": "dev-skills",
90
90
  "repoOnly": false,
91
91
  "files": {
92
- "SKILL.md": "561e0232d20fc7e6893b38a4c3d7deb41862d60d7b545a98d3ce7ba1d3c7ac06",
92
+ "SKILL.md": "bb5f2f34d7342052c5c97b628d11976fafc61996c320f568f863c466606284bf",
93
93
  "agents/openai.yaml": "3f35d3dc60f179d4ac2526997c4d10173ba3018d06f0dbe77e209ce2978fa702",
94
94
  "assets/review-known-patterns.md.template": "7747b807ed18c4c8bf8acdc6481b2f7f27914d932d757c7243a32a41a18d32c5",
95
95
  "references/conventions.md": "95bf916cc11f95f3d5eefcb34db4fbc05d88ae1c8f08869800dfa417f8da17ed",
96
96
  "references/cross-agent.md": "3541715d0563703373ea3d93582119b80356d1cbdee19ce17f958c0f7c914884",
97
97
  "references/dispatch-prompts.md": "cb8134a7b6ff9e20ec4db02d0af2f8767f71bf13fa9a21b28b7a586c2747ec1d",
98
98
  "references/security-axis.md": "e207071fc48f761f2deb24c73ef4f5ebcf28c130264dab7c3061ae6c0eee6543",
99
- "refresh/REFRESH.md": "242c1efe630327abe12bf9db4c4085aaf05b262a4dbd885b12b0f29cdd4a285d",
100
- "refresh/sources.json": "a7ba2340964dc70410626087686145b5790a0e763a18ef2c4d9aa37e950f2e04",
101
- "scripts/skill-scan.mjs": "2d74ebd63ffd66cca5208b5dab2984ace47ef4e832e188f6811ff7c2e303095d"
99
+ "refresh/REFRESH.md": "ae22dc0ecf756310f4c3aa7e2b20d13b94fb26595fb3b4432eb26cbc6fd7b27d",
100
+ "refresh/sources.json": "2bae54e726ecb43b778eabe5b62bde51d5725d2a58e700940727f275cd5afe16",
101
+ "scripts/lib/skillspector.mjs": "f4311c0ca61d3a54bf3012817b317667ae783c0ccd8ffc22b5fcb8674e9f937e",
102
+ "scripts/skill-scan.mjs": "6638c4ea9aaa29524c58946ea69a4149a156cd39491f5c30f028e53ac402f31c"
102
103
  }
103
104
  },
104
105
  "dev-setup": {
105
106
  "group": "dev-skills",
106
107
  "repoOnly": false,
107
108
  "files": {
108
- "SKILL.md": "4b29049b8ce165fb03f554c8784e095cbe97cef16752b57b86321352a1017ce7",
109
+ "SKILL.md": "17140c694fa2c0fe231f94df85863d2ed69876f61a31a719ca1458821367373e",
109
110
  "agents/openai.yaml": "be790c04fdf910e71642cfadf1a7c07c217bf7fad18581610feb4c31a79651f2",
110
111
  "assets/agents-section.md.template": "6c9591d50f73e1e05e78c8969c7836767432cf32487af7205b2cb9f4a410440e",
111
- "assets/dev-profile.md.template": "26e244b488d181e23ff4b49addd9ab519c8f5d43e2d914638033648a7ca5bd3a",
112
+ "assets/dev-profile.md.template": "b81517d1b982f5d69e4416e939e2935191adbc396dba8c75d18767672fd95e28",
112
113
  "references/conventions.md": "95bf916cc11f95f3d5eefcb34db4fbc05d88ae1c8f08869800dfa417f8da17ed",
113
114
  "references/harness-facts.md": "2be1d81f6a616d40dc865aaeefaf53c61bc13ade4ba1de8fb406c520bf15be40",
114
115
  "references/stack-playbooks.md": "8f457c3d0f96e3c99d745b04332590fdf8af53a9694541181d9c878ae0718ea2",
@@ -159,7 +160,7 @@
159
160
  "files": {
160
161
  "SKILL.md": "310ac32ff4d557cc166a470fd1866060dfc5f03898e8ff3da9c406b84580742b",
161
162
  "agents/openai.yaml": "df320cf32a8db06c213f7892844dab810b7e96cfbf32502d25185d4a4167ef87",
162
- "assets/templates/README.md.template": "49dccb4e9b5bdb07c254f9142502b3fd11fd5b083d8661b0ae6a567dec79de74",
163
+ "assets/templates/README.md.template": "74750e59514bff2211b043f6584ed75271529ff4b304e82b207c282396d3e2a1",
163
164
  "assets/templates/REFRESH.md.template": "ff86afade565d668333e7f6d0432213760fd68a62b8ba22897a8601b74c94ac0",
164
165
  "assets/templates/SKILL.md.template": "b038775f97b13b54227fe41a767a48cdaa922a93d1732f22c2d1d79bff52ca00",
165
166
  "assets/templates/openai.yaml.template": "2fbf488369825109d6cad4f17e7b729a40d9e3c43c7f01bc5dfa16361d59c086",
@@ -170,7 +171,7 @@
170
171
  "references/eval-playbook.md": "1f463dfea8d69b7022712776794369892643aa020186fe3a3707c493a6c2d4e9",
171
172
  "refresh/REFRESH.md": "695f3283b27c14accf4095137898ce1f6dc40749483c5a5705e80aa9693f08c2",
172
173
  "refresh/sources.json": "e554e80c72c16dc70587361804db9e0cf2716c128820b2612d5983a009b2b242",
173
- "scripts/scaffold-skill.mjs": "849370bd152e488c66753882928cdb18db356f21468647c8e0a51ee58a385183"
174
+ "scripts/scaffold-skill.mjs": "d9edbe912ca20aad29afe1b0b904f85bbc4c9bfefb2c75e2986949ce00cb7fae"
174
175
  }
175
176
  }
176
177
  }