skilldrop-cli 0.15.1 → 0.16.1
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 +2 -1
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +33 -2
- package/packs/converters/skills/md-to-docx/manifest.json +2 -1
- package/packs/converters/skills/md-to-docx/scripts/md_to_docx.py +3 -1
- package/packs/converters/skills/md-to-html/manifest.json +1 -0
- package/packs/converters/skills/md-to-xlsx/SKILL.md +2 -1
- package/packs/converters/skills/md-to-xlsx/manifest.json +2 -1
- package/packs/converters/skills/md-to-xlsx/scripts/md_to_xlsx.py +18 -4
- 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 +2 -1
- package/packs/dev-team/skills/pre-merge-review/scripts/gate.py +4 -1
- 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.1",
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "file-to-markdown",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json or .pdf and says \"read this\", \"turn this into markdown\", \"extract the text\", or wants it critiqued or briefed.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -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."
|
|
@@ -56,6 +56,7 @@ class Report:
|
|
|
56
56
|
def md_escape(text):
|
|
57
57
|
text = text.replace("\\", "\\\\")
|
|
58
58
|
text = re.sub(r"([*_`\[\]])", r"\\\1", text)
|
|
59
|
+
text = re.sub(r"<(?=[A-Za-z/!])", r"\\<", text) # literal <tag> text must not read as raw HTML
|
|
59
60
|
return text
|
|
60
61
|
|
|
61
62
|
|
|
@@ -330,7 +331,7 @@ def docx_to_md(path, rep):
|
|
|
330
331
|
if tcpr is not None and tcpr.find(W + "gridSpan") is not None:
|
|
331
332
|
row.extend([""] * (int(tcpr.find(W + "gridSpan").get(W + "val", "1")) - 1))
|
|
332
333
|
rows.append(row)
|
|
333
|
-
if
|
|
334
|
+
if len(list(tbl.iter(W + "tbl"))) > 1:
|
|
334
335
|
rep.notes.append("a nested table was flattened into its parent cell")
|
|
335
336
|
rep.keep("tables")
|
|
336
337
|
return md_table(rows)
|
|
@@ -339,6 +340,19 @@ def docx_to_md(path, rep):
|
|
|
339
340
|
return "".join((n.text or "") if n.tag == W + "t" else "\t" for n in p.iter()
|
|
340
341
|
if n.tag in (W + "t", W + "tab"))
|
|
341
342
|
|
|
343
|
+
def style_name(p):
|
|
344
|
+
ppr = p.find(W + "pPr")
|
|
345
|
+
ps = ppr.find(W + "pStyle") if ppr is not None else None
|
|
346
|
+
if ps is None:
|
|
347
|
+
return ""
|
|
348
|
+
sid = ps.get(W + "val", "")
|
|
349
|
+
return styles.get(sid, (sid.lower(), None))[0]
|
|
350
|
+
|
|
351
|
+
def code_text(p):
|
|
352
|
+
"""Text of a code paragraph: line breaks and tabs kept, no inline formatting or escaping."""
|
|
353
|
+
return "".join((n.text or "") if n.tag == W + "t" else "\t" if n.tag == W + "tab" else "\n"
|
|
354
|
+
for n in p.iter() if n.tag in (W + "t", W + "tab", W + "br", W + "cr"))
|
|
355
|
+
|
|
342
356
|
def run_size(p):
|
|
343
357
|
"""(largest font size in half-points, every text run bold?) for unstyled documents."""
|
|
344
358
|
sizes, bold = [0], True
|
|
@@ -402,7 +416,19 @@ def docx_to_md(path, rep):
|
|
|
402
416
|
if not text:
|
|
403
417
|
prev_list = False if not li else prev_list
|
|
404
418
|
continue
|
|
405
|
-
if lvl
|
|
419
|
+
sname = "" if lvl or li else style_name(el)
|
|
420
|
+
if re.match(r"(source ?code|code|html preformatted|preformatted text|macro text)$", sname):
|
|
421
|
+
# one code block may be one paragraph with line breaks or a run of one-line paragraphs
|
|
422
|
+
if blocks and blocks[-1][0] == "code":
|
|
423
|
+
blocks[-1] = ("code", blocks[-1][1] + "\n" + code_text(el))
|
|
424
|
+
else:
|
|
425
|
+
blocks.append(("code", code_text(el)))
|
|
426
|
+
rep.keep("code blocks")
|
|
427
|
+
prev_list = False
|
|
428
|
+
elif "quote" in sname or sname == "block text":
|
|
429
|
+
blocks.append(("block", "> " + text.replace("\n", "\n> ")))
|
|
430
|
+
prev_list = False
|
|
431
|
+
elif lvl:
|
|
406
432
|
rep.keep("headings")
|
|
407
433
|
blocks.append(("block", "#" * lvl + " " + text.replace(" \n", " ")))
|
|
408
434
|
prev_list = False
|
|
@@ -439,6 +465,11 @@ def docx_to_md(path, rep):
|
|
|
439
465
|
for k, (kind, text) in enumerate(blocks):
|
|
440
466
|
if k and not (kind == "list" and blocks[k - 1][0] == "list"):
|
|
441
467
|
out.append("")
|
|
468
|
+
if kind == "code":
|
|
469
|
+
fence = "```"
|
|
470
|
+
while fence in text:
|
|
471
|
+
fence += "`"
|
|
472
|
+
text = fence + "\n" + text + "\n" + fence
|
|
442
473
|
out.append(text)
|
|
443
474
|
return "\n".join(out) + "\n"
|
|
444
475
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-to-docx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn a Markdown draft into a Word document (.docx) that someone outside engineering can open, edit and track changes in, with real Heading styles so the navigation pane and table of contents work, live hyperlinks, numbered and bulleted lists, tables, code blocks, embedded images and optional brand fonts and colours, and a warning for everything that could not be carried over. Use when the user wants a Markdown file as a Word doc, says \"send this as a .docx\", \"convert to Word\", \"legal needs it in Word\", or \"make this editable for the client\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -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."
|
|
@@ -34,6 +34,8 @@ WARNINGS = []
|
|
|
34
34
|
|
|
35
35
|
def warn(line, msg):
|
|
36
36
|
where = "line %d: " % line if line else ""
|
|
37
|
+
if where + msg in WARNINGS: # a big table repeats one warning per cell; say it once
|
|
38
|
+
return
|
|
37
39
|
WARNINGS.append(where + msg)
|
|
38
40
|
print("WARN %s%s" % (where, msg), file=sys.stderr)
|
|
39
41
|
|
|
@@ -942,7 +944,7 @@ class Doc(object):
|
|
|
942
944
|
|
|
943
945
|
|
|
944
946
|
def static_parts(title):
|
|
945
|
-
now = datetime.datetime.
|
|
947
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
946
948
|
ct = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
|
947
949
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
|
|
948
950
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
@@ -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."
|
|
@@ -78,7 +78,8 @@ nothing. Formulas stay off unless the request says otherwise, noted as an `[assu
|
|
|
78
78
|
| Anything else, including `n/a`, `1.234,50`, `30/09/2026` | Text |
|
|
79
79
|
|
|
80
80
|
In Markdown cells, `**bold**`, `` `code` ``, links and images are reduced to their text, and
|
|
81
|
-
`<br>` becomes a line break inside a wrapped cell. JSON numbers and booleans keep their type
|
|
81
|
+
`<br>` becomes a line break inside a wrapped cell. JSON numbers and booleans keep their type,
|
|
82
|
+
except integers over 15 digits, which become text.
|
|
82
83
|
JSON strings stay text except ISO dates, because whoever produced the JSON chose a string.
|
|
83
84
|
|
|
84
85
|
## Useful references in this skill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-to-xlsx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn the tables in a Markdown file, a CSV or a JSON array into an Excel workbook (.xlsx) that finance, ops or a client can sort, filter and sum, with one sheet per table named from its heading, real numbers, percentages, currency and dates instead of text, a bold frozen header row with filters, sensible column widths, and formulas left inert unless asked for. Use when the user wants a spreadsheet from Markdown tables or data, says \"put this in Excel\", \"convert the CSV to xlsx\", \"give finance a spreadsheet\", or \"turn this JSON into a sheet\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -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."
|
|
@@ -170,8 +170,11 @@ def type_text(s, formulas, stats, tz_flag, infer_numbers=True):
|
|
|
170
170
|
text = "0"
|
|
171
171
|
return Cell("n", text, fmt, width)
|
|
172
172
|
if RE_SCI.match(s):
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
val = float(s)
|
|
174
|
+
# 1e999 overflows to inf, which is not a number Excel can read: keep it as text
|
|
175
|
+
if val == val and val not in (float("inf"), float("-inf")):
|
|
176
|
+
stats["numbers"] += 1
|
|
177
|
+
return Cell("n", num_text(val), None, width)
|
|
175
178
|
if len(raw) > MAX_CELL:
|
|
176
179
|
stats["truncated"] += 1
|
|
177
180
|
s = s[:MAX_CELL]
|
|
@@ -189,6 +192,10 @@ def type_json(v, formulas, stats, tz_flag):
|
|
|
189
192
|
if v != v or v in (float("inf"), float("-inf")):
|
|
190
193
|
stats["text"] += 1
|
|
191
194
|
return Cell("s", str(v), None, len(str(v)))
|
|
195
|
+
if isinstance(v, int) and len(str(abs(v))) > 15:
|
|
196
|
+
# Excel keeps 15 significant digits; a longer integer is an ID, and rounding it is data loss
|
|
197
|
+
stats["text"] += 1
|
|
198
|
+
return Cell("s", str(v), None, len(str(v)))
|
|
192
199
|
stats["numbers"] += 1
|
|
193
200
|
return Cell("n", num_text(v), None, len(num_text(v)))
|
|
194
201
|
if isinstance(v, (dict, list)):
|
|
@@ -230,6 +237,9 @@ def split_row(line):
|
|
|
230
237
|
return cells
|
|
231
238
|
|
|
232
239
|
|
|
240
|
+
HTML_TAGS = [0] # tags removed by strip_md, reported once per file
|
|
241
|
+
|
|
242
|
+
|
|
233
243
|
def strip_md(s):
|
|
234
244
|
"""Plain text from a Markdown table cell."""
|
|
235
245
|
s = re.sub(r"<br\s*/?>", "\n", s, flags=re.I)
|
|
@@ -240,7 +250,8 @@ def strip_md(s):
|
|
|
240
250
|
s = re.sub(r"(?<![\w*])\*(?!\s)(.+?)(?<!\s)\*(?![\w*])", r"\1", s)
|
|
241
251
|
s = re.sub(r"(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)", r"\1", s)
|
|
242
252
|
s = re.sub(r"~~(.+?)~~", r"\1", s)
|
|
243
|
-
s = re.
|
|
253
|
+
s, n = re.subn(r"</?[A-Za-z][^>]*>", "", s)
|
|
254
|
+
HTML_TAGS[0] += n
|
|
244
255
|
s = re.sub(r"\\([\\`*_{}\[\]()#+\-.!|~])", r"\1", s)
|
|
245
256
|
return s
|
|
246
257
|
|
|
@@ -280,6 +291,9 @@ def read_markdown(text):
|
|
|
280
291
|
tables.append((heading, header, rows, start))
|
|
281
292
|
continue
|
|
282
293
|
i += 1
|
|
294
|
+
if HTML_TAGS[0]:
|
|
295
|
+
warn("%d HTML tag(s) removed from headings and cells (text between them kept); write a literal "
|
|
296
|
+
"<...> as <...> or in backticks to keep it" % HTML_TAGS[0])
|
|
283
297
|
return tables
|
|
284
298
|
|
|
285
299
|
|
|
@@ -533,7 +547,7 @@ def write_xlsx(out, sheets, title, formulas):
|
|
|
533
547
|
'<bookViews><workbookView activeTab="0"/></bookViews><sheets>%s</sheets>'
|
|
534
548
|
'<definedNames>%s</definedNames>%s</workbook>') % (
|
|
535
549
|
"".join(sheet_tags), "".join(names), '<calcPr fullCalcOnLoad="1"/>' if formulas else "")
|
|
536
|
-
now = datetime.datetime.
|
|
550
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
537
551
|
core = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
|
538
552
|
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
|
|
539
553
|
'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" '
|
|
@@ -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": [] },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pre-merge-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Gate an existing change for merge — run one skill that fires the whole production-readiness pass. First a deterministic mechanical gate (lint + typecheck + tests, via a script whose exit code decides), then the three-reviewer panel dispatched in parallel — devils-advocate (correctness), security-reviewer (exploitability), code-quality (craft) — then a single READY / NOT READY verdict. Use before opening a PR or merging, when you want the full review fleet run on a diff you already have. Do NOT use to implement a feature from a spec (that's feature-implement-loop) or to review a document (that's doc-critique).",
|
|
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
|
}
|
|
@@ -112,7 +112,10 @@ def install_hook(root, cmd, config):
|
|
|
112
112
|
body += "\n" + _hook_block(cmd, config)
|
|
113
113
|
with open(path, "w", encoding="utf-8") as fh:
|
|
114
114
|
fh.write(body)
|
|
115
|
-
|
|
115
|
+
# chmod +x, not a fixed 0o755: add execute only where the umask already granted read, so a
|
|
116
|
+
# restrictive umask (077) keeps the hook private instead of being widened to world-readable.
|
|
117
|
+
mode = os.stat(path).st_mode
|
|
118
|
+
os.chmod(path, mode | ((mode & 0o444) >> 2))
|
|
116
119
|
print(f"gate: installed a blocking pre-commit hook -> {path}")
|
|
117
120
|
print(" a RED gate now blocks `git commit` (any tool). Bypass once with --no-verify; "
|
|
118
121
|
"wire CI required-checks for the un-bypassable backstop.")
|
|
@@ -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."
|