skilldrop-cli 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/skilldrop.js +158 -28
- package/contracts/skill.schema.json +15 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/ai-usage-report/manifest.json +1 -0
- package/packs/converters/skills/file-to-markdown/manifest.json +1 -0
- package/packs/converters/skills/md-to-docx/manifest.json +1 -0
- package/packs/converters/skills/md-to-html/manifest.json +1 -0
- package/packs/converters/skills/md-to-xlsx/manifest.json +1 -0
- package/packs/converters/skills/mermaid-render/manifest.json +1 -0
- package/packs/core/skills/output-hygiene/manifest.json +1 -0
- package/packs/data-analytics/skills/sql-review/manifest.json +1 -0
- package/packs/design/skills/brand-kit/manifest.json +1 -0
- package/packs/design/skills/deck-builder/manifest.json +1 -0
- package/packs/design/skills/marketing-flyer/manifest.json +1 -0
- package/packs/dev-team/skills/contribution-wizard/SKILL.md +2 -0
- package/packs/dev-team/skills/contribution-wizard/manifest.json +1 -1
- package/packs/dev-team/skills/pre-merge-review/manifest.json +1 -0
- package/packs/experience-design/skills/content-design/manifest.json +1 -0
- package/packs/grc/skills/risk-register/manifest.json +1 -0
- package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +1 -0
- package/packs/research/skills/hypothesis-comparison/manifest.json +1 -0
- package/packs/research/skills/source-synthesis/manifest.json +1 -0
- package/packs/skill-engineering/skills/skill-review/manifest.json +1 -0
- package/packs/solution-architect/skills/figma-diagrams/manifest.json +1 -0
- package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -0
- package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -0
- package/packs/trackers/skills/backlog-triage/manifest.json +1 -0
- package/packs/trackers/skills/team-status-report/manifest.json +1 -0
- package/packs/trackers/skills/tracker-brief-sync/manifest.json +1 -0
package/bin/skilldrop.js
CHANGED
|
@@ -51,9 +51,11 @@ Usage:
|
|
|
51
51
|
(--no-skills for the loop alone) (RFC-0028)
|
|
52
52
|
skilldrop install --panel review install the review panel — the 3 reviewer subagents +
|
|
53
53
|
the pre-merge-review orchestrator that fires them (RFC-0020)
|
|
54
|
-
skilldrop update [--force]
|
|
54
|
+
skilldrop update [--force] [--changed] re-copy installed skills whose version changed; files
|
|
55
55
|
you edited are kept and the new copy lands beside them
|
|
56
|
-
as <file>.upstream (--force overwrites) (RFC-0032)
|
|
56
|
+
as <file>.upstream (--force overwrites) (RFC-0032).
|
|
57
|
+
Files changed upstream under the same version are named,
|
|
58
|
+
not taken, unless you add --changed (RFC-0039)
|
|
57
59
|
skilldrop outdated show installed vs current versions, change nothing
|
|
58
60
|
skilldrop diff <skill> [--stat] what differs between your installed copy and the catalog's —
|
|
59
61
|
your edits, upstream changes, or both
|
|
@@ -81,7 +83,8 @@ Catalogs:
|
|
|
81
83
|
(default) the catalog bundled with this package
|
|
82
84
|
--from <dir|url> a skilldrop catalog (packs/<pack>/skills/<name>/ or flat skills/<name>/) OR
|
|
83
85
|
an agentbundle one (packs/<pack>/.apm/skills/<name>/SKILL.md); a git URL
|
|
84
|
-
works for either
|
|
86
|
+
works for either. Append #<commit-sha> to pin exactly (a branch or tag can
|
|
87
|
+
move after you review it); installs record the commit they got
|
|
85
88
|
|
|
86
89
|
Install/update/uninstall targets (pick one):
|
|
87
90
|
(default) ~/.claude/skills Claude Code, user scope
|
|
@@ -150,29 +153,68 @@ function parseArgs(argv) {
|
|
|
150
153
|
manifestOf; never touch cat.skillsDir directly (it exists only on the flat skilldrop shape). */
|
|
151
154
|
|
|
152
155
|
const catalogCache = {};
|
|
156
|
+
/* Commit pinning (RFC-0039, OWASP AST02/AST07). A tag or branch can be moved to point at
|
|
157
|
+
different code after you reviewed it; a commit SHA cannot. `--from <url>#<sha>` fetches
|
|
158
|
+
exactly that commit, and every install records the commit it actually got, so the ledger
|
|
159
|
+
says what is on disk and an unpinned install can tell you how to pin it. */
|
|
160
|
+
const SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
161
|
+
function gitHead(dir) {
|
|
162
|
+
try {
|
|
163
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
164
|
+
} catch (e) { return null; }
|
|
165
|
+
}
|
|
166
|
+
function splitSource(source) {
|
|
167
|
+
const i = source.lastIndexOf("#");
|
|
168
|
+
return i > 0 ? [source.slice(0, i), source.slice(i + 1)] : [source, ""];
|
|
169
|
+
}
|
|
170
|
+
function isPinned(source) { return !!source && SHA_RE.test(splitSource(source)[1]); }
|
|
171
|
+
|
|
172
|
+
function fetchCatalog(source) {
|
|
173
|
+
const [url, ref] = splitSource(source);
|
|
174
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skilldrop-cat-"));
|
|
175
|
+
const git = (args, cwd) => execFileSync("git", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
|
|
176
|
+
try {
|
|
177
|
+
if (SHA_RE.test(ref)) {
|
|
178
|
+
// `git clone --branch` takes branches and tags, not commits: fetch the commit itself.
|
|
179
|
+
git(["init", "-q"], dir);
|
|
180
|
+
git(["remote", "add", "origin", url], dir);
|
|
181
|
+
let direct = ref.length === 40;
|
|
182
|
+
if (direct) {
|
|
183
|
+
try { git(["fetch", "-q", "--depth", "1", "origin", ref], dir); }
|
|
184
|
+
catch (e) { direct = false; } // a host that won't serve a commit by itself
|
|
185
|
+
}
|
|
186
|
+
if (!direct) git(["fetch", "-q", "origin", "+refs/heads/*:refs/remotes/origin/*", "+refs/tags/*:refs/tags/*"], dir);
|
|
187
|
+
try { git(["checkout", "-q", direct ? "FETCH_HEAD" : ref], dir); }
|
|
188
|
+
catch (e) { die(`catalog '${source}': commit ${ref} isn't in that repository (check the SHA, or that it was pushed)`); }
|
|
189
|
+
} else {
|
|
190
|
+
git(["clone", "-q", "--depth", "1", ...(ref ? ["--branch", ref] : []), url, dir]);
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
die(`could not fetch catalog '${source}' — not a local path, and git ${SHA_RE.test(ref) ? "could not fetch that commit" : "clone failed"}`);
|
|
194
|
+
}
|
|
195
|
+
const head = gitHead(dir);
|
|
196
|
+
if (SHA_RE.test(ref) && (!head || !head.startsWith(ref.toLowerCase())))
|
|
197
|
+
die(`catalog '${source}' resolved to ${head || "nothing"}, not the pinned commit ${ref}`);
|
|
198
|
+
return dir;
|
|
199
|
+
}
|
|
200
|
+
|
|
153
201
|
function resolveCatalog(source) {
|
|
154
202
|
const key = source || BUNDLED;
|
|
155
203
|
if (catalogCache[key]) return catalogCache[key];
|
|
156
204
|
let dir;
|
|
157
205
|
if (!source) dir = ROOT;
|
|
158
206
|
else if (fs.existsSync(source)) dir = path.resolve(source);
|
|
159
|
-
else
|
|
160
|
-
const [url, ref] = source.split("#");
|
|
161
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "skilldrop-cat-"));
|
|
162
|
-
try {
|
|
163
|
-
execFileSync("git", ["clone", "--depth", "1", ...(ref ? ["--branch", ref] : []), url, dir], { stdio: ["ignore", "ignore", "pipe"] });
|
|
164
|
-
} catch (e) {
|
|
165
|
-
die(`could not fetch catalog '${source}' — not a local path, and git clone failed`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
207
|
+
else dir = fetchCatalog(source);
|
|
168
208
|
const src = source || BUNDLED;
|
|
209
|
+
// The bundled catalog is versioned by the npm package; anything else by its git commit.
|
|
210
|
+
const meta = { commit: source ? gitHead(dir) : null, pinned: isPinned(source) };
|
|
169
211
|
const packsDir = path.join(dir, "packs");
|
|
170
212
|
if (fs.existsSync(packsDir) && fs.readdirSync(packsDir).some((p) => fs.existsSync(path.join(packsDir, p, "pack.json"))))
|
|
171
|
-
return (catalogCache[key] = readPacksCatalog(dir, src));
|
|
213
|
+
return (catalogCache[key] = Object.assign(readPacksCatalog(dir, src), meta));
|
|
172
214
|
if (fs.existsSync(path.join(dir, "skills")))
|
|
173
|
-
return (catalogCache[key] = { dir, source: src, shape: "skilldrop", skillsDir: path.join(dir, "skills") });
|
|
215
|
+
return (catalogCache[key] = Object.assign({ dir, source: src, shape: "skilldrop", skillsDir: path.join(dir, "skills") }, meta));
|
|
174
216
|
if (fs.existsSync(path.join(dir, "packs")))
|
|
175
|
-
return (catalogCache[key] = readApmCatalog(dir, src));
|
|
217
|
+
return (catalogCache[key] = Object.assign(readApmCatalog(dir, src), meta));
|
|
176
218
|
die(`'${src}' is not a catalog — expected packs/<pack>/pack.json, a skills/ directory, or packs/<pack>/pack.toml (agentbundle)`);
|
|
177
219
|
}
|
|
178
220
|
|
|
@@ -389,7 +431,7 @@ const SCRIPT_RULES = [
|
|
|
389
431
|
{ id: "shell-exec", sev: "🟧", owasp: ["AST03", "LLM06"], why: "executes shell commands",
|
|
390
432
|
re: /\b(os\.system|subprocess\.(run|call|Popen|check_output)|child_process|execSync|spawnSync|shell_exec|`[^`\n]*\$\()/ },
|
|
391
433
|
{ id: "network", sev: "🟧", owasp: ["AST03", "LLM02"], why: "makes outbound network calls",
|
|
392
|
-
re: /\b(requests\.(get|post|put)|urllib\.request|httpx\.|axios\.|node-fetch|\bfetch\s*\(\s*["'`]https?:|curl\s+https?:|wget\s+https?:)/i },
|
|
434
|
+
re: /\b(requests\.(get|post|put|patch|delete|head|request|Session)\b|urllib\.request|urlopen\s*\(|http\.client|httpx\.|aiohttp\.|axios\.|node-fetch|\bfetch\s*\(\s*["'`]https?:|curl\s+https?:|wget\s+https?:)/i },
|
|
393
435
|
{ id: "credentials", sev: "🟧", owasp: ["AST01", "LLM02"], why: "reads credentials or secret material",
|
|
394
436
|
re: /\b(os\.environ|process\.env)\b[^\n]{0,40}(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)|~\/\.(aws|ssh|npmrc|netrc)|\.env\b|id_rsa/i },
|
|
395
437
|
{ id: "broad-fs", sev: "🟨", owasp: ["AST03", "LLM06"], why: "writes outside the skill's own directory",
|
|
@@ -441,10 +483,47 @@ function scanSkill(cat, s) {
|
|
|
441
483
|
}
|
|
442
484
|
});
|
|
443
485
|
}
|
|
486
|
+
findings.push(...permissionGaps(cat, s, findings));
|
|
444
487
|
const order = { "🟥": 0, "🟧": 1, "🟨": 2, "⚪": 3 };
|
|
445
488
|
return findings.sort((a, b) => order[a.sev] - order[b.sev]);
|
|
446
489
|
}
|
|
447
490
|
|
|
491
|
+
/* RFC-0039: compare what a skill's scripts do (the scan's script findings) with what its
|
|
492
|
+
manifest says they do. A capability the code shows and the manifest doesn't declare is the
|
|
493
|
+
finding that matters most: it is either an honest omission or something hiding. */
|
|
494
|
+
function permissionsOf(cat, s) {
|
|
495
|
+
if (cat.shape === "apm") return null;
|
|
496
|
+
try { return manifestOf(cat, s).permissions || null; } catch (e) { return null; }
|
|
497
|
+
}
|
|
498
|
+
function permissionGaps(cat, s, findings) {
|
|
499
|
+
const dir = skillDir(cat, s);
|
|
500
|
+
if (!dir || !fs.existsSync(path.join(dir, "scripts"))) return [];
|
|
501
|
+
const perm = permissionsOf(cat, s);
|
|
502
|
+
if (!perm)
|
|
503
|
+
return [{ sev: "🟨", id: "no-permissions", owasp: ["AST03", "AST10"], file: "manifest.json", line: 0,
|
|
504
|
+
why: "ships scripts but declares no permissions (network, commands, files) — nothing to check them against",
|
|
505
|
+
text: "add a `permissions` block to manifest.json (RFC-0039)" }];
|
|
506
|
+
const code = findings.filter((f) => SCRIPT_EXT.has(path.extname(f.file).toLowerCase()));
|
|
507
|
+
const gaps = [];
|
|
508
|
+
const gap = (id, f, why) => gaps.push({ sev: "🟥", id, owasp: ["AST03", "AST01"], why, file: f.file, line: f.line, text: f.text });
|
|
509
|
+
for (const f of code) {
|
|
510
|
+
if ((f.id === "network" || f.id === "exec-remote") && !(perm.network || []).length)
|
|
511
|
+
gap("undeclared-network", f, "makes network calls, but the manifest declares no network hosts");
|
|
512
|
+
if ((f.id === "shell-exec" || f.id === "exec-remote") && !(perm.commands || []).length)
|
|
513
|
+
gap("undeclared-commands", f, "runs commands, but the manifest declares none");
|
|
514
|
+
if (f.id === "broad-fs" && perm.files !== "anywhere")
|
|
515
|
+
gap("undeclared-files", f, `writes outside its own paths, but the manifest declares files: "${perm.files}"`);
|
|
516
|
+
}
|
|
517
|
+
return gaps;
|
|
518
|
+
}
|
|
519
|
+
function permissionSummary(perm) {
|
|
520
|
+
if (!perm) return "no permissions declared";
|
|
521
|
+
const net = (perm.network || []).length ? `contacts ${perm.network.join(", ")}` : "no network";
|
|
522
|
+
const cmds = (perm.commands || []).length ? `runs ${perm.commands.map((c) => c === "*" ? "commands you configure" : c).join(", ")}` : "runs no commands";
|
|
523
|
+
const files = { none: "writes nothing", "named-paths": "writes only paths you name", project: "writes inside the project", anywhere: "writes anywhere" }[perm.files] || `files: ${perm.files}`;
|
|
524
|
+
return `${net} · ${cmds} · ${files}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
448
527
|
function printScan(cat, names, { compact = false } = {}) {
|
|
449
528
|
let total = 0, worst = null;
|
|
450
529
|
for (const s of names) {
|
|
@@ -916,6 +995,7 @@ function copyOne(cat, s, dest, ide, l, notes) {
|
|
|
916
995
|
const note = writeWiring(ide, dest, s, m.description);
|
|
917
996
|
if (note && notes) notes.push(note);
|
|
918
997
|
l.data[s] = { version: m.version, source: cat.source, files: fileHashes(skillDir(cat, s)) };
|
|
998
|
+
if (cat.commit) l.data[s].commit = cat.commit;
|
|
919
999
|
return m;
|
|
920
1000
|
}
|
|
921
1001
|
|
|
@@ -974,6 +1054,7 @@ function mergeOne(cat, s, dest, ide, l, notes, dry = false) {
|
|
|
974
1054
|
const note = writeWiring(ide, dest, s, m.description);
|
|
975
1055
|
if (note && notes) notes.push(note);
|
|
976
1056
|
l.data[s] = { version: m.version, source: cat.source, files: next };
|
|
1057
|
+
if (cat.commit) l.data[s].commit = cat.commit;
|
|
977
1058
|
return { m, kept, orphaned };
|
|
978
1059
|
}
|
|
979
1060
|
|
|
@@ -1110,10 +1191,20 @@ function install(args) {
|
|
|
1110
1191
|
console.log(`\n${withHooks.join(", ")} declare hooks — re-run with --with-hooks to wire them (git pre-commit reminders / session-start context).`);
|
|
1111
1192
|
}
|
|
1112
1193
|
|
|
1194
|
+
if (cat.source !== BUNDLED && cat.commit) {
|
|
1195
|
+
if (cat.pinned) console.log(`\nPinned: these skills came from commit ${cat.commit.slice(0, 12)}, as requested.`);
|
|
1196
|
+
else console.log(`\nNot pinned: '${cat.source}' can change after you review it. These skills came from commit ${cat.commit.slice(0, 12)};` +
|
|
1197
|
+
`\n to get exactly these files again, install with --from ${splitSource(cat.source)[0]}#${cat.commit}`);
|
|
1198
|
+
}
|
|
1113
1199
|
if (cat.source !== BUNDLED) {
|
|
1114
1200
|
console.log(`\nWARNING: third-party catalog '${cat.source}'. Skills are instructions your AI agent will follow — review each installed SKILL.md under ${dest} before first use. Install copied files only; nothing was executed.`);
|
|
1115
1201
|
// Supply-chain scan (RFC-0022): surface risky patterns at the moment of trust. Reports only —
|
|
1116
1202
|
// the files are already copied, and a match is a prompt to read, not a verdict.
|
|
1203
|
+
const scripted = names.filter((s) => fs.existsSync(path.join(skillDir(cat, s), "scripts")));
|
|
1204
|
+
if (scripted.length) {
|
|
1205
|
+
console.log(`\nWhat the scripts declare they do (RFC-0039):`);
|
|
1206
|
+
for (const s of scripted) console.log(` ${s}: ${permissionSummary(permissionsOf(cat, s))}`);
|
|
1207
|
+
}
|
|
1117
1208
|
const { total } = printScan(cat, names, { compact: true });
|
|
1118
1209
|
console.log(total
|
|
1119
1210
|
? `\n${total} pattern(s) worth reading before you trust these skills. Full detail: skilldrop scan --from ${cat.source}`
|
|
@@ -1159,12 +1250,22 @@ function installedRows(flags) {
|
|
|
1159
1250
|
const l = ledger(dest);
|
|
1160
1251
|
const rows = Object.keys(l.data).sort().map((s) => {
|
|
1161
1252
|
const src = lsrc(l.data[s]);
|
|
1162
|
-
let current = null, cat = null;
|
|
1253
|
+
let current = null, cat = null, changed = [];
|
|
1254
|
+
const entry = l.data[s];
|
|
1163
1255
|
try {
|
|
1164
1256
|
cat = resolveCatalog(src === BUNDLED ? undefined : src);
|
|
1165
|
-
if (skillExists(cat, s))
|
|
1257
|
+
if (skillExists(cat, s)) {
|
|
1258
|
+
current = manifestOf(cat, s).version;
|
|
1259
|
+
// Same version, different files: the catalog changed the skill without bumping it.
|
|
1260
|
+
// The version check alone would never see that (OWASP AST07, update drift).
|
|
1261
|
+
if (current === lver(entry) && typeof entry === "object" && entry.files) {
|
|
1262
|
+
const now = fileHashes(skillDir(cat, s));
|
|
1263
|
+
changed = [...new Set([...Object.keys(now), ...Object.keys(entry.files)])]
|
|
1264
|
+
.filter((f) => now[f] !== entry.files[f]).sort();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1166
1267
|
} catch (e) { /* unreachable source: current stays null */ }
|
|
1167
|
-
return { s, src, cat, installed: lver(
|
|
1268
|
+
return { s, src, cat, installed: lver(entry), current, changed, pinned: isPinned(src) };
|
|
1168
1269
|
});
|
|
1169
1270
|
return { dest, ide, l, rows };
|
|
1170
1271
|
}
|
|
@@ -1173,7 +1274,10 @@ function update(args) {
|
|
|
1173
1274
|
const { dest, ide, l, rows } = installedRows(args.flags);
|
|
1174
1275
|
if (!rows.length) return console.log(`nothing installed at ${dest}`);
|
|
1175
1276
|
const dry = !!args.flags["dry-run"];
|
|
1176
|
-
|
|
1277
|
+
// A row is due when its version moved, or (with --changed) when its files moved under the
|
|
1278
|
+
// same version. Same-version changes are never taken silently: they are named and skipped.
|
|
1279
|
+
const isDue = (r) => r.current && (r.current !== r.installed || (args.flags.changed && r.changed.length));
|
|
1280
|
+
const due = rows.filter(isDue);
|
|
1177
1281
|
if (args.flags.force && !dry) {
|
|
1178
1282
|
const lost = due.flatMap((r) => editedFiles(dest, r.s, l.data[r.s]).map((f) => `${r.s}/${f}`));
|
|
1179
1283
|
if (lost.length) {
|
|
@@ -1183,19 +1287,20 @@ function update(args) {
|
|
|
1183
1287
|
}
|
|
1184
1288
|
if (dry) return planUpdate(dest, ide, l, rows, args.flags);
|
|
1185
1289
|
let n = 0, conflicts = 0;
|
|
1290
|
+
const silent = rows.filter((r) => r.current === r.installed && r.changed.length && !args.flags.changed);
|
|
1186
1291
|
for (const r of rows) {
|
|
1187
1292
|
if (!r.current) { console.log(`skip ${r.s}: source '${r.src}' unreachable or skill gone from it`); continue; }
|
|
1188
|
-
if (r
|
|
1293
|
+
if (!isDue(r)) continue;
|
|
1189
1294
|
const problems = checkSkill(r.cat, r.s);
|
|
1190
1295
|
if (problems.length) { console.log(`skip ${r.s}: fails structural check in '${r.src}' (${problems[0]})`); continue; }
|
|
1191
1296
|
// No baseline (installed before RFC-0032) or --force: overwrite as before, and record one.
|
|
1192
1297
|
const entry = l.data[r.s];
|
|
1193
1298
|
if (args.flags.force || typeof entry !== "object" || !entry.files) {
|
|
1194
1299
|
copyOne(r.cat, r.s, dest, ide, l);
|
|
1195
|
-
console.log(`updated ${r.s} ${r.installed} -> ${r.current} (${r.src})`);
|
|
1300
|
+
console.log(`updated ${r.s} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
|
|
1196
1301
|
} else {
|
|
1197
1302
|
const { kept, orphaned } = mergeOne(r.cat, r.s, dest, ide, l);
|
|
1198
|
-
console.log(`updated ${r.s} ${r.installed} -> ${r.current} (${r.src})`);
|
|
1303
|
+
console.log(`updated ${r.s} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
|
|
1199
1304
|
for (const f of kept) console.log(` kept your edits in ${f} — new version at ${f}.upstream`);
|
|
1200
1305
|
for (const f of orphaned) console.log(` kept ${f} — dropped upstream, but you edited it`);
|
|
1201
1306
|
conflicts += kept.length;
|
|
@@ -1203,10 +1308,17 @@ function update(args) {
|
|
|
1203
1308
|
n++;
|
|
1204
1309
|
}
|
|
1205
1310
|
saveLedger(l);
|
|
1206
|
-
console.log(n ? `\n${n} skill(s) updated.` : "everything up to date.");
|
|
1311
|
+
console.log(n ? `\n${n} skill(s) updated.` : (silent.length ? "no version changes." : "everything up to date."));
|
|
1312
|
+
if (silent.length)
|
|
1313
|
+
console.log(`\nNot taken: ${silent.map((r) => r.s).join(", ")} changed upstream without a version bump.` +
|
|
1314
|
+
`\n Read the change (skilldrop diff <skill>), then take it with: skilldrop update --changed`);
|
|
1315
|
+
const pinned = rows.filter((r) => r.pinned);
|
|
1316
|
+
if (pinned.length)
|
|
1317
|
+
console.log(`\n${pinned.length} skill(s) come from a catalog pinned to a commit, so they stay as they are. To move them,` +
|
|
1318
|
+
`\n reinstall with the new commit: skilldrop install <skill> --from <url>#<new-commit>`);
|
|
1207
1319
|
// An update is a fresh act of trust in a third-party catalog (OWASP AST07): scan what changed.
|
|
1208
1320
|
const thirdParty = {};
|
|
1209
|
-
for (const r of rows) if (r
|
|
1321
|
+
for (const r of rows) if (isDue(r) && r.src !== BUNDLED) (thirdParty[r.src] = thirdParty[r.src] || { cat: r.cat, names: [] }).names.push(r.s);
|
|
1210
1322
|
for (const { cat, names } of Object.values(thirdParty)) {
|
|
1211
1323
|
const { total } = printScan(cat, names, { compact: true });
|
|
1212
1324
|
console.log(total ? `\n${total} pattern(s) in the updated skills from '${cat.source}' worth reading. Full detail: skilldrop scan --from ${cat.source}`
|
|
@@ -1222,7 +1334,11 @@ function planUpdate(dest, ide, l, rows, flags) {
|
|
|
1222
1334
|
let n = 0;
|
|
1223
1335
|
for (const r of rows) {
|
|
1224
1336
|
if (!r.current) { console.log(`would skip ${r.s}: source '${r.src}' unreachable or skill gone from it`); continue; }
|
|
1225
|
-
if (r.current === r.installed) continue;
|
|
1337
|
+
if (r.current === r.installed && !r.changed.length) continue;
|
|
1338
|
+
if (r.current === r.installed && !flags.changed) {
|
|
1339
|
+
console.log(`would not take ${r.s}: ${r.changed.length} file(s) changed upstream without a version bump (add --changed to take them)`);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1226
1342
|
const problems = checkSkill(r.cat, r.s);
|
|
1227
1343
|
if (problems.length) { console.log(`would skip ${r.s}: fails structural check (${problems[0]})`); continue; }
|
|
1228
1344
|
n++;
|
|
@@ -1237,25 +1353,34 @@ function planUpdate(dest, ide, l, rows, flags) {
|
|
|
1237
1353
|
for (const f of kept) console.log(` would keep your edits in ${f} and write the new version to ${f}.upstream`);
|
|
1238
1354
|
for (const f of orphaned) console.log(` would keep ${f} — dropped upstream, but you edited it`);
|
|
1239
1355
|
}
|
|
1240
|
-
|
|
1356
|
+
const held = rows.filter((r) => r.current && r.current === r.installed && r.changed.length && !flags.changed).length;
|
|
1357
|
+
console.log(n ? `\n${n} skill(s) would update. Rerun without --dry-run to apply.` : (held ? "no version changes." : "everything up to date."));
|
|
1241
1358
|
}
|
|
1242
1359
|
|
|
1243
1360
|
function outdated(args) {
|
|
1244
1361
|
const { dest, rows } = installedRows(args.flags);
|
|
1245
1362
|
const stale = rows.filter((r) => r.current && r.current !== r.installed);
|
|
1363
|
+
const drift = rows.filter((r) => r.changed && r.changed.length);
|
|
1246
1364
|
if (args.flags.json)
|
|
1247
1365
|
return emitJSON({
|
|
1248
1366
|
dest,
|
|
1249
1367
|
count: rows.length,
|
|
1250
1368
|
outdatedCount: stale.length,
|
|
1369
|
+
changedWithoutVersionCount: drift.length,
|
|
1251
1370
|
skills: rows.map((r) => ({
|
|
1252
1371
|
name: r.s, installed: r.installed, current: r.current,
|
|
1253
1372
|
source: r.src, outdated: !!(r.current && r.current !== r.installed),
|
|
1373
|
+
changedWithoutVersion: r.changed || [], pinned: r.pinned,
|
|
1254
1374
|
})),
|
|
1255
1375
|
});
|
|
1256
1376
|
if (!rows.length) return console.log(`nothing installed at ${dest}`);
|
|
1257
1377
|
for (const r of stale) console.log(`${r.s}: installed ${r.installed}, current ${r.current} (${r.src})`);
|
|
1258
|
-
|
|
1378
|
+
for (const r of drift)
|
|
1379
|
+
console.log(`${r.s}: ${r.changed.length} file(s) changed in '${r.src}' without a version bump (${r.changed.slice(0, 3).join(", ")}${r.changed.length > 3 ? ", …" : ""})`);
|
|
1380
|
+
if (drift.length)
|
|
1381
|
+
console.log(`\n${drift.length} skill(s) changed upstream with the same version. Read the change first (skilldrop diff <skill>),` +
|
|
1382
|
+
`\nthen take it with: skilldrop update --changed`);
|
|
1383
|
+
console.log(stale.length ? `\n${stale.length} outdated — run: skilldrop update` : (drift.length ? "" : "everything up to date."));
|
|
1259
1384
|
}
|
|
1260
1385
|
|
|
1261
1386
|
function uninstall(args) {
|
|
@@ -1890,6 +2015,7 @@ function info(args) {
|
|
|
1890
2015
|
deps: { pip: (m.deps || {}).pip || [], npm: (m.deps || {}).npm || [], requirementsTxt: needsPip },
|
|
1891
2016
|
env: { required: (m.env || {}).required || [], optional: (m.env || {}).optional || [] },
|
|
1892
2017
|
hooks: m.hooks || [],
|
|
2018
|
+
permissions: m.permissions || null,
|
|
1893
2019
|
});
|
|
1894
2020
|
console.log(`${m.name}@${m.version} (tier: ${(m.model && m.model.tier) || "n/a"})\n\n${m.description}\n`);
|
|
1895
2021
|
console.log(`catalog: ${cat.source}`);
|
|
@@ -1898,6 +2024,8 @@ function info(args) {
|
|
|
1898
2024
|
if (((m.deps || {}).pip || []).length || fs.existsSync(path.join(skillDir(cat, s), "requirements.txt")))
|
|
1899
2025
|
console.log("deps: python (requirements.txt)");
|
|
1900
2026
|
if (((m.env || {}).required || []).length) console.log(`env: ${m.env.required.join(", ")} (required)`);
|
|
2027
|
+
if (fs.existsSync(path.join(skillDir(cat, s), "scripts")))
|
|
2028
|
+
console.log(`scripts: ${permissionSummary(m.permissions)}${m.permissions && m.permissions.notes ? `\n ${m.permissions.notes}` : ""}`);
|
|
1901
2029
|
}
|
|
1902
2030
|
|
|
1903
2031
|
function listPacks(args) {
|
|
@@ -1924,6 +2052,8 @@ function validateCmd(args) {
|
|
|
1924
2052
|
let bad = 0;
|
|
1925
2053
|
for (const s of names) {
|
|
1926
2054
|
const problems = checkSkill(cat, s);
|
|
2055
|
+
for (const f of scanSkill(cat, s).filter((x) => x.id.startsWith("undeclared-")))
|
|
2056
|
+
problems.push(`${f.file}:${f.line} ${f.why} (RFC-0039)`);
|
|
1927
2057
|
for (const p of problems) console.log(`FAIL ${s}: ${p}`);
|
|
1928
2058
|
if (problems.length) bad++;
|
|
1929
2059
|
}
|
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
"description": "Vars that change behaviour but do not block." }
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
+
"permissions": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["network", "commands", "files"],
|
|
45
|
+
"description": "RFC-0039 (OWASP AST03, AST10). What the skill's scripts do, declared so a reader and `skilldrop scan` can compare it with what the code does. Required for every skill that ships scripts/.",
|
|
46
|
+
"properties": {
|
|
47
|
+
"network": { "type": "array", "items": { "type": "string" },
|
|
48
|
+
"description": "Hosts the scripts contact, e.g. api.figma.com. Empty means none." },
|
|
49
|
+
"commands": { "type": "array", "items": { "type": "string" },
|
|
50
|
+
"description": "External programs the scripts run, e.g. pdftotext. \"*\" means commands the user configures. Empty means none." },
|
|
51
|
+
"files": { "type": "string", "enum": ["none", "named-paths", "project", "anywhere"],
|
|
52
|
+
"description": "What the scripts write: nothing; only paths the user names; inside the current project; or anywhere." },
|
|
53
|
+
"notes": { "type": "string", "description": "Anything a reviewer should know that the fields can't say." }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
41
56
|
"related": {
|
|
42
57
|
"type": "array",
|
|
43
58
|
"items": { "type": "string" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Portable AI-agent skills for the deliverables knowledge workers ship \u2014 diagrams, ADRs, PRDs, runbooks, decks, reviews \u2014 installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
"env": { "required": [], "optional": [] },
|
|
8
8
|
"related": [],
|
|
9
9
|
"tags": ["ai-usage", "reporting", "telemetry", "adoption", "observability", "manager-tools"],
|
|
10
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
10
11
|
"model": { "tier": "standard", "rationale": "Tabulates telemetry; the 'AI theater' effectiveness call needs some judgment." }
|
|
11
12
|
}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"pdf",
|
|
42
42
|
"extract"
|
|
43
43
|
],
|
|
44
|
+
"permissions": {"network": [], "commands": ["pdftotext"], "files": "named-paths", "notes": "pdftotext (Poppler) is used only for .pdf input, and only if it is installed."},
|
|
44
45
|
"model": {
|
|
45
46
|
"tier": "light",
|
|
46
47
|
"rationale": "A deterministic stdlib script does the extraction; the model runs it, relays the dropped-content report and hands off. No generation beyond a short summary of losses."
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"document",
|
|
45
45
|
"export"
|
|
46
46
|
],
|
|
47
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
47
48
|
"model": {
|
|
48
49
|
"tier": "standard",
|
|
49
50
|
"rationale": "A stdlib script does the conversion; the model restructures the Markdown so it converts cleanly (heading levels, footnotes, HTML), reads each warning and explains the losses. Bounded editing, no open-ended generation."
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"export",
|
|
36
36
|
"brand"
|
|
37
37
|
],
|
|
38
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "The script makes no network calls. With --mermaid cdn, the HTML it writes loads Mermaid from cdn.jsdelivr.net when someone opens it."},
|
|
38
39
|
"model": {
|
|
39
40
|
"tier": "light",
|
|
40
41
|
"rationale": "A deterministic stdlib script does the conversion; the model picks options, fixes a few unsupported Markdown constructs and relays the script's warnings. Little judgment."
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"markdown",
|
|
26
26
|
"convert"
|
|
27
27
|
],
|
|
28
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
28
29
|
"model": {
|
|
29
30
|
"tier": "standard",
|
|
30
31
|
"rationale": "A stdlib script types and writes the cells; the model shapes tables from the source (headings, units, one header row), decides whether formulas are safe to enable, and checks the per-sheet type counts against the data. Bounded, mostly mechanical judgment."
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"png",
|
|
33
33
|
"render"
|
|
34
34
|
],
|
|
35
|
+
"permissions": {"network": [], "commands": ["mmdc"], "files": "named-paths", "notes": "mmdc (mermaid-cli) renders SVG, PNG and PDF if it is installed. Without it the script writes an HTML page that loads Mermaid from cdn.jsdelivr.net when opened; the script itself makes no network calls."},
|
|
35
36
|
"model": {
|
|
36
37
|
"tier": "standard",
|
|
37
38
|
"rationale": "The script lints and renders deterministically, but fixing a broken diagram means reading the author's intent and rewriting syntax without changing nodes, edges or labels, and knowing when to ask instead."
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"ai-detection",
|
|
27
27
|
"pre-ship"
|
|
28
28
|
],
|
|
29
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "Writes only with --fix, which rewrites the files it was given, in place."},
|
|
29
30
|
"model": {
|
|
30
31
|
"tier": "standard",
|
|
31
32
|
"rationale": "The mechanical pass is a deterministic script; the model does bounded rewriting of flagged prose against a fixed tell list, not adversarial review."
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"bigquery",
|
|
35
35
|
"snowflake"
|
|
36
36
|
],
|
|
37
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
37
38
|
"model": {
|
|
38
39
|
"tier": "heavy",
|
|
39
40
|
"rationale": "Adversarial correctness review: finding join fan-out, NULL and timezone bugs means reasoning about data the query never shows, and a missed finding ships a wrong number. Never downgrade."
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"design",
|
|
38
38
|
"style-guide"
|
|
39
39
|
],
|
|
40
|
+
"permissions": {"network": [], "commands": [], "files": "none", "notes": "The checker reads brand.json and the files it names, and writes nothing."},
|
|
40
41
|
"model": {
|
|
41
42
|
"tier": "standard",
|
|
42
43
|
"rationale": "Extracts and normalises brand facts from files and pages, then runs a checker. Bounded synthesis."
|
|
@@ -13,5 +13,6 @@
|
|
|
13
13
|
"pip": "python3 -m pip install -r requirements.txt"
|
|
14
14
|
},
|
|
15
15
|
"tags": ["powerpoint", "pptx", "deck", "presentation", "template", "charts", "stakeholder-communication"],
|
|
16
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "Needs the python-pptx package (declared in deps)."},
|
|
16
17
|
"model": { "tier": "standard", "rationale": "Shapes content, maps a brand template's layouts, and runs a script. Judgment, not hard reasoning." }
|
|
17
18
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"brand",
|
|
33
33
|
"design"
|
|
34
34
|
],
|
|
35
|
+
"permissions": {"network": [], "commands": ["google-chrome", "chromium", "chrome"], "files": "named-paths", "notes": "A local Chrome or Chromium is run headless only for --pdf and --png; the HTML needs nothing."},
|
|
35
36
|
"model": {
|
|
36
37
|
"tier": "standard",
|
|
37
38
|
"rationale": "Writes short copy to fit and fills a spec for a deterministic builder. Bounded generation."
|
|
@@ -46,6 +46,8 @@ After intake, produce the following in a single response, each in its own fenced
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
If the skill ships `scripts/`, add a `permissions` block before `model`: the hosts the scripts contact, the external programs they run, and where they write (`"none"`, `"named-paths"`, `"project"` or `"anywhere"`). For example: `"permissions": { "network": [], "commands": ["pdftotext"], "files": "named-paths" }`. Validation fails a script skill without it, and fails one whose code does something it doesn't declare.
|
|
50
|
+
|
|
49
51
|
Tier guidance:
|
|
50
52
|
- **light**: pattern matching, formatting, extraction with no judgment calls
|
|
51
53
|
- **standard**: synthesis, judgment calls, structured output requiring reasoning (most skills)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contribution-wizard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Guides an author through creating a new skilldrop skill from scratch — generates the manifest, SKILL.md, eval cases, and skill-catalogue entry from a plain-language description of the skill's purpose. Use when authoring a new skill for this repo or a private fork, ensuring a new skill meets the schema and validation requirements before submission.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
{ "to": "council-review", "when": "the blocker is a design disagreement, not a defect", "purpose": "Adjudicate the disagreement outside the diff.", "fallback": "If council-review is absent, state each position and its strongest counter inline, and name the crux the decision turns on." }
|
|
11
11
|
],
|
|
12
12
|
"tags": ["dev-workflow", "code-review", "pre-merge", "quality-gate", "subagents", "production-readiness"],
|
|
13
|
+
"permissions": {"network": [], "commands": ["git", "ruff", "mypy", "pytest", "*"], "files": "project", "notes": "Runs the lint, typecheck and test commands configured for the project, through a shell, by design. --install-hook writes a git pre-commit hook in the repository."},
|
|
13
14
|
"model": { "tier": "standard", "rationale": "Coordination: runs a deterministic gate script, dispatches the (heavy) reviewer subagents, and renders a rule-based go/no-go. The adversarial reasoning is delegated to the panel, so the skill itself is orchestration, not deep judgment." }
|
|
14
15
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"ux",
|
|
34
34
|
"user-needs"
|
|
35
35
|
],
|
|
36
|
+
"permissions": {"network": [], "commands": [], "files": "none", "notes": "Prints readability figures; writes nothing."},
|
|
36
37
|
"model": {
|
|
37
38
|
"tier": "standard",
|
|
38
39
|
"rationale": "Rewrites source text from user needs under fixed plain-language rules, with a script for the readability numbers. Prioritising questions needs judgment, but facts come from the source."
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"governance",
|
|
36
36
|
"compliance"
|
|
37
37
|
],
|
|
38
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
38
39
|
"model": {
|
|
39
40
|
"tier": "standard",
|
|
40
41
|
"rationale": "Structured writing against a fixed format and scale, with a script doing the checks and counts. Scoring needs some judgment but is bounded by the written scale."
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"iam",
|
|
41
41
|
"drift"
|
|
42
42
|
],
|
|
43
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values."},
|
|
43
44
|
"model": {
|
|
44
45
|
"tier": "heavy",
|
|
45
46
|
"rationale": "Adversarial review of a destructive change: judging which replacements lose data, which access changes widen exposure, and what is out of scope needs careful reasoning; a missed destroy costs far more than the model."
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"analysis",
|
|
36
36
|
"evidence"
|
|
37
37
|
],
|
|
38
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
38
39
|
"model": {
|
|
39
40
|
"tier": "heavy",
|
|
40
41
|
"rationale": "Judgment-heavy: generating real rivals, rating every evidence-hypothesis cell honestly and resisting the favourite theory is where a fast pass fails; the script only does the arithmetic."
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"citations",
|
|
35
35
|
"evidence"
|
|
36
36
|
],
|
|
37
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
37
38
|
"model": {
|
|
38
39
|
"tier": "standard",
|
|
39
40
|
"rationale": "Bounded synthesis over supplied text with a fixed citation format and strength criteria; the citation checker catches the mechanical failures, so mid-tier judgment is enough."
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"security",
|
|
43
43
|
"owasp"
|
|
44
44
|
],
|
|
45
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
45
46
|
"model": {
|
|
46
47
|
"tier": "heavy",
|
|
47
48
|
"rationale": "Adversarial audit with a verdict: routing collisions and paraphrased safety issues are what a fast pass misses, and the lint script covers only the mechanical half."
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"design",
|
|
29
29
|
"api"
|
|
30
30
|
],
|
|
31
|
+
"permissions": {"network": ["api.figma.com"], "commands": [], "files": "named-paths", "notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API."},
|
|
31
32
|
"model": {
|
|
32
33
|
"tier": "standard",
|
|
33
34
|
"rationale": "API calls + spec generation. Bounded transformation."
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"savings",
|
|
42
42
|
"sre"
|
|
43
43
|
],
|
|
44
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
44
45
|
"model": {
|
|
45
46
|
"tier": "standard",
|
|
46
47
|
"rationale": "A stdlib script does the aggregation; the model maps columns, checks evidence and computes savings with shown arithmetic against a fixed category checklist, which is bounded judgment."
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"engineering-metrics",
|
|
41
41
|
"reporting"
|
|
42
42
|
],
|
|
43
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
43
44
|
"model": {
|
|
44
45
|
"tier": "standard",
|
|
45
46
|
"rationale": "A stdlib script computes every number; the model maps columns, checks data quality and interprets weekly trends against fixed DORA definitions, which is bounded judgment."
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"refinement",
|
|
35
35
|
"duplicates"
|
|
36
36
|
],
|
|
37
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
37
38
|
"model": {
|
|
38
39
|
"tier": "standard",
|
|
39
40
|
"rationale": "A script does the counting; the model confirms duplicates by reading, judges readiness and writes a reasoned ordering. Bounded judgment over structured data."
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"rag",
|
|
26
26
|
"stakeholder-update"
|
|
27
27
|
],
|
|
28
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths"},
|
|
28
29
|
"model": {
|
|
29
30
|
"tier": "standard",
|
|
30
31
|
"rationale": "A script computes every count and the suggested RAG; the model writes plain-language summaries, attributes outside facts and frames risks and asks. Bounded writing over computed numbers."
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"traceability",
|
|
34
34
|
"drift"
|
|
35
35
|
],
|
|
36
|
+
"permissions": {"network": [], "commands": [], "files": "named-paths", "notes": "The github target writes a shell script of gh commands for the user to run; it runs nothing itself."},
|
|
36
37
|
"model": {
|
|
37
38
|
"tier": "standard",
|
|
38
39
|
"rationale": "Maps doc sections to items and writes acceptance criteria that restate requirements; a script checks the spec, writes the import file and computes drift. Bounded transformation with a deterministic check."
|