@vegastack/skills 0.16.2 → 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/package.json +1 -1
- package/skill/dev-review/SKILL.md +2 -0
- package/skill/dev-review/refresh/REFRESH.md +16 -1
- package/skill/dev-review/refresh/sources.json +130 -3
- package/skill/dev-review/scripts/lib/skillspector.mjs +221 -0
- package/skill/dev-review/scripts/skill-scan.mjs +227 -7
- package/skill/dev-setup/SKILL.md +2 -2
- package/skill/dev-setup/assets/dev-profile.md.template +1 -0
- package/skill-integrity.json +7 -6
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
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.
|
|
535
|
-
|
|
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)`);
|
package/skill/dev-setup/SKILL.md
CHANGED
|
@@ -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) |
|
|
30
|
+
| SkillSpector (skill scanning) | nothing to detect — dev-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
|
package/skill-integrity.json
CHANGED
|
@@ -89,26 +89,27 @@
|
|
|
89
89
|
"group": "dev-skills",
|
|
90
90
|
"repoOnly": false,
|
|
91
91
|
"files": {
|
|
92
|
-
"SKILL.md": "
|
|
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": "
|
|
100
|
-
"refresh/sources.json": "
|
|
101
|
-
"scripts/
|
|
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": "
|
|
109
|
+
"SKILL.md": "17140c694fa2c0fe231f94df85863d2ed69876f61a31a719ca1458821367373e",
|
|
109
110
|
"agents/openai.yaml": "be790c04fdf910e71642cfadf1a7c07c217bf7fad18581610feb4c31a79651f2",
|
|
110
111
|
"assets/agents-section.md.template": "6c9591d50f73e1e05e78c8969c7836767432cf32487af7205b2cb9f4a410440e",
|
|
111
|
-
"assets/dev-profile.md.template": "
|
|
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",
|