skilldrop-cli 0.16.10 → 0.16.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,7 @@ In Claude Code you can use the plugin marketplace instead: run `/plugin marketpl
62
62
 
63
63
  ## Go deeper
64
64
 
65
- - **Use it:** [the operating model](guides/explanation/operating-model.md) · [docs portal](https://sananthanarayan.github.io/skilldrop/docs/) · [a guide for every pack](guides/README.md#per-pack) · [install in any IDE](guides/how-to/install-per-ide.md) · [try skills in a repo you don't own](guides/how-to/install-per-ide.md#trying-skills-in-a-repo-you-dont-own) · [profiles](guides/how-to/profiles.md) · [hooks](guides/how-to/wire-a-hook.md)
65
+ - **Use it:** [the operating model](guides/explanation/operating-model.md) · [docs portal](https://sananthanarayan.github.io/skilldrop/docs/) · [a guide for every pack](guides/README.md#per-pack) · [install in any IDE](guides/how-to/install-per-ide.md) · [try skills in a repo you don't own](guides/how-to/install-per-ide.md#try-skills-in-a-repo-you-dont-own) · [profiles](guides/how-to/profiles.md) · [hooks](guides/how-to/wire-a-hook.md)
66
66
  - **Run it in CI:** [a skill on every pull request](guides/how-to/run-a-skill-in-ci.md) · [how skills are checked](https://sananthanarayan.github.io/skilldrop/evals/) · [benchmark skills yourself](guides/how-to/benchmark-skills.md) · [measure your loops](guides/how-to/measure-your-loops.md)
67
67
  - **Roll it out:** [across your org, including an internal mirror](guides/how-to/enterprise-distribution.md) · [publish your own catalogue](guides/how-to/publish-a-catalogue.md) · [security and the OWASP Top 10s](guides/reference/owasp-mapping.md)
68
68
  - **Reviewers:** three subagents that push back on generated work: `devils-advocate`, `security-reviewer` and `code-quality`. See [`agents/`](agents/README.md).
package/bin/skilldrop.js CHANGED
@@ -51,13 +51,13 @@ 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] [--changed] re-copy installed skills whose version changed; files
54
+ skilldrop update [--force] [--changed] re-copy installed skills and loops whose version changed; files
55
55
  you edited are kept and the new copy lands beside them
56
56
  as <file>.upstream (--force overwrites) (RFC-0032).
57
57
  Files changed upstream under the same version are named,
58
58
  not taken, unless you add --changed (RFC-0039)
59
59
  skilldrop outdated show installed vs current versions, change nothing
60
- skilldrop diff <skill> [--stat] what differs between your installed copy and the catalog's —
60
+ skilldrop diff <name> [--stat] what differs between your installed skill or loop and the catalog's —
61
61
  your edits, upstream changes, or both
62
62
  skilldrop doctor check a target against its ledger: missing folders, leftover
63
63
  .upstream files, stale wiring and hooks. Changes nothing.
@@ -1015,16 +1015,50 @@ function fileHashes(dir) {
1015
1015
  return out;
1016
1016
  }
1017
1017
 
1018
+ /* A loop installs as <name>/SKILL.md (its LOOP.md) plus <name>/loop.json. update, outdated,
1019
+ diff and doctor treat a ledger row marked `loop` like a skill through these helpers: which
1020
+ files it installs and where the catalog keeps each one, its hashes, metadata and check. */
1021
+ function loopFiles(cat, n) {
1022
+ const d = loopDir(cat, n);
1023
+ return { "SKILL.md": path.join(d, "LOOP.md"), "loop.json": path.join(d, "loop.json") };
1024
+ }
1025
+ function unitHashes(cat, s, isLoop) {
1026
+ if (!isLoop) return fileHashes(skillDir(cat, s));
1027
+ const out = {};
1028
+ for (const [rel, p] of Object.entries(loopFiles(cat, s))) out[rel] = sha256(fs.readFileSync(p));
1029
+ return out;
1030
+ }
1031
+ function unitSource(cat, s, isLoop, rel) {
1032
+ return isLoop ? loopFiles(cat, s)[rel] : path.join(skillDir(cat, s), ...rel.split("/"));
1033
+ }
1034
+ function unitExists(cat, s, isLoop) { return isLoop ? loopsIn(cat).includes(s) : skillExists(cat, s); }
1035
+ function unitMeta(cat, s, isLoop) { return isLoop ? loopSpec(cat, s) : manifestOf(cat, s); }
1036
+ function unitCheck(cat, s, isLoop) { return isLoop ? checkLoop(cat, s) : checkSkill(cat, s); }
1037
+ function isLoopEntry(entry) { return !!(entry && typeof entry === "object" && entry.loop); }
1038
+ function unitLabel(r) { return r.loop ? `loop ${r.s}` : r.s; }
1039
+
1040
+ function copyLoop(cat, n, dest, ide, l, notes) {
1041
+ const sp = loopSpec(cat, n);
1042
+ const out = path.join(dest, n);
1043
+ fs.mkdirSync(out, { recursive: true });
1044
+ for (const [rel, p] of Object.entries(loopFiles(cat, n))) fs.copyFileSync(p, path.join(out, rel));
1045
+ const note = writeWiring(ide, dest, n, sp.description);
1046
+ if (note && notes) notes.push(note);
1047
+ l.data[n] = { version: sp.version || null, source: cat.source, loop: true, files: unitHashes(cat, n, true) };
1048
+ if (cat.commit) l.data[n].commit = cat.commit;
1049
+ return sp;
1050
+ }
1051
+
1018
1052
  /* Update one skill file by file against the ledger baseline (RFC-0032):
1019
1053
  upstream unchanged -> leave the local file alone, edited or not
1020
1054
  local untouched, or new file -> take the new version
1021
1055
  local edited, upstream moved -> keep the local file, write the new one as <file>.upstream
1022
1056
  A file the catalog dropped is removed only if the user never touched it. The new baseline is
1023
1057
  what the catalog ships now, so a kept edit stays "edited" until the user merges it. */
1024
- function mergeOne(cat, s, dest, ide, l, notes, dry = false) {
1025
- const m = manifestOf(cat, s);
1026
- const src = skillDir(cat, s), dst = path.join(dest, s);
1027
- const base = l.data[s].files, next = fileHashes(src);
1058
+ function mergeOne(cat, s, dest, ide, l, notes, dry = false, isLoop = false) {
1059
+ const m = unitMeta(cat, s, isLoop);
1060
+ const dst = path.join(dest, s);
1061
+ const base = l.data[s].files, next = unitHashes(cat, s, isLoop);
1028
1062
  const kept = [], orphaned = [];
1029
1063
  const at = (root, rel) => path.join(root, ...rel.split("/"));
1030
1064
  const localHash = (rel) => { try { return sha256(fs.readFileSync(at(dst, rel))); } catch (e) { return null; } };
@@ -1035,13 +1069,13 @@ function mergeOne(cat, s, dest, ide, l, notes, dry = false) {
1035
1069
  if (mine === null ? !(rel in base) : mine === base[rel]) {
1036
1070
  if (dry) continue;
1037
1071
  fs.mkdirSync(path.dirname(to), { recursive: true });
1038
- fs.copyFileSync(at(src, rel), to);
1072
+ fs.copyFileSync(unitSource(cat, s, isLoop, rel), to);
1039
1073
  fs.rmSync(to + ".upstream", { force: true });
1040
1074
  } else {
1041
1075
  kept.push(rel);
1042
1076
  if (dry) continue;
1043
1077
  fs.mkdirSync(path.dirname(to), { recursive: true });
1044
- fs.copyFileSync(at(src, rel), to + ".upstream");
1078
+ fs.copyFileSync(unitSource(cat, s, isLoop, rel), to + ".upstream");
1045
1079
  }
1046
1080
  }
1047
1081
  for (const rel of Object.keys(base)) {
@@ -1053,7 +1087,8 @@ function mergeOne(cat, s, dest, ide, l, notes, dry = false) {
1053
1087
  if (dry) return { m, kept, orphaned };
1054
1088
  const note = writeWiring(ide, dest, s, m.description);
1055
1089
  if (note && notes) notes.push(note);
1056
- l.data[s] = { version: m.version, source: cat.source, files: next };
1090
+ l.data[s] = { version: m.version || null, source: cat.source, files: next };
1091
+ if (isLoop) l.data[s].loop = true;
1057
1092
  if (cat.commit) l.data[s].commit = cat.commit;
1058
1093
  return { m, kept, orphaned };
1059
1094
  }
@@ -1252,20 +1287,21 @@ function installedRows(flags) {
1252
1287
  const src = lsrc(l.data[s]);
1253
1288
  let current = null, cat = null, changed = [];
1254
1289
  const entry = l.data[s];
1290
+ const loop = isLoopEntry(entry);
1255
1291
  try {
1256
1292
  cat = resolveCatalog(src === BUNDLED ? undefined : src);
1257
- if (skillExists(cat, s)) {
1258
- current = manifestOf(cat, s).version;
1293
+ if (unitExists(cat, s, loop)) {
1294
+ current = unitMeta(cat, s, loop).version || null;
1259
1295
  // Same version, different files: the catalog changed the skill without bumping it.
1260
1296
  // The version check alone would never see that (OWASP AST07, update drift).
1261
1297
  if (current === lver(entry) && typeof entry === "object" && entry.files) {
1262
- const now = fileHashes(skillDir(cat, s));
1298
+ const now = unitHashes(cat, s, loop);
1263
1299
  changed = [...new Set([...Object.keys(now), ...Object.keys(entry.files)])]
1264
1300
  .filter((f) => now[f] !== entry.files[f]).sort();
1265
1301
  }
1266
1302
  }
1267
1303
  } catch (e) { /* unreachable source: current stays null */ }
1268
- return { s, src, cat, installed: lver(entry), current, changed, pinned: isPinned(src) };
1304
+ return { s, src, cat, loop, installed: lver(entry), current, changed, pinned: isPinned(src) };
1269
1305
  });
1270
1306
  return { dest, ide, l, rows };
1271
1307
  }
@@ -1289,18 +1325,20 @@ function update(args) {
1289
1325
  let n = 0, conflicts = 0;
1290
1326
  const silent = rows.filter((r) => r.current === r.installed && r.changed.length && !args.flags.changed);
1291
1327
  for (const r of rows) {
1292
- if (!r.current) { console.log(`skip ${r.s}: source '${r.src}' unreachable or skill gone from it`); continue; }
1328
+ if (!r.current) { console.log(`skip ${unitLabel(r)}: source '${r.src}' unreachable, ${r.loop ? "loop" : "skill"} gone from it, or no version to compare`); continue; }
1293
1329
  if (!isDue(r)) continue;
1294
- const problems = checkSkill(r.cat, r.s);
1295
- if (problems.length) { console.log(`skip ${r.s}: fails structural check in '${r.src}' (${problems[0]})`); continue; }
1296
- // No baseline (installed before RFC-0032) or --force: overwrite as before, and record one.
1330
+ const problems = unitCheck(r.cat, r.s, r.loop);
1331
+ if (problems.length) { console.log(`skip ${unitLabel(r)}: fails structural check in '${r.src}' (${problems[0]})`); continue; }
1332
+ // No baseline (installed before RFC-0032, or a loop installed before 0.16.11) or --force:
1333
+ // overwrite as before, and record one.
1297
1334
  const entry = l.data[r.s];
1298
1335
  if (args.flags.force || typeof entry !== "object" || !entry.files) {
1299
- copyOne(r.cat, r.s, dest, ide, l);
1300
- console.log(`updated ${r.s} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
1336
+ if (r.loop) copyLoop(r.cat, r.s, dest, ide, l);
1337
+ else copyOne(r.cat, r.s, dest, ide, l);
1338
+ console.log(`updated ${unitLabel(r)} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
1301
1339
  } else {
1302
- const { kept, orphaned } = mergeOne(r.cat, r.s, dest, ide, l);
1303
- console.log(`updated ${r.s} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
1340
+ const { kept, orphaned } = mergeOne(r.cat, r.s, dest, ide, l, null, false, r.loop);
1341
+ console.log(`updated ${unitLabel(r)} ${r.installed === r.current ? `${r.current} (same version, changed upstream)` : `${r.installed} -> ${r.current}`} (${r.src})`);
1304
1342
  for (const f of kept) console.log(` kept your edits in ${f} — new version at ${f}.upstream`);
1305
1343
  for (const f of orphaned) console.log(` kept ${f} — dropped upstream, but you edited it`);
1306
1344
  conflicts += kept.length;
@@ -1308,7 +1346,7 @@ function update(args) {
1308
1346
  n++;
1309
1347
  }
1310
1348
  saveLedger(l);
1311
- console.log(n ? `\n${n} skill(s) updated.` : (silent.length ? "no version changes." : "everything up to date."));
1349
+ console.log(n ? `\n${n} skill(s) or loop(s) updated.` : (silent.length ? "no version changes." : "everything up to date."));
1312
1350
  if (silent.length)
1313
1351
  console.log(`\nNot taken: ${silent.map((r) => r.s).join(", ")} changed upstream without a version bump.` +
1314
1352
  `\n Read the change (skilldrop diff <skill>), then take it with: skilldrop update --changed`);
@@ -1333,28 +1371,28 @@ function planUpdate(dest, ide, l, rows, flags) {
1333
1371
  console.log(`dry run — nothing will be written. Target: ${dest} (${ide})\n`);
1334
1372
  let n = 0;
1335
1373
  for (const r of rows) {
1336
- if (!r.current) { console.log(`would skip ${r.s}: source '${r.src}' unreachable or skill gone from it`); continue; }
1374
+ if (!r.current) { console.log(`would skip ${unitLabel(r)}: source '${r.src}' unreachable, ${r.loop ? "loop" : "skill"} gone from it, or no version to compare`); continue; }
1337
1375
  if (r.current === r.installed && !r.changed.length) continue;
1338
1376
  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)`);
1377
+ console.log(`would not take ${unitLabel(r)}: ${r.changed.length} file(s) changed upstream without a version bump (add --changed to take them)`);
1340
1378
  continue;
1341
1379
  }
1342
- const problems = checkSkill(r.cat, r.s);
1343
- if (problems.length) { console.log(`would skip ${r.s}: fails structural check (${problems[0]})`); continue; }
1380
+ const problems = unitCheck(r.cat, r.s, r.loop);
1381
+ if (problems.length) { console.log(`would skip ${unitLabel(r)}: fails structural check (${problems[0]})`); continue; }
1344
1382
  n++;
1345
1383
  const entry = l.data[r.s];
1346
- console.log(`would update ${r.s} ${r.installed} -> ${r.current} (${r.src})`);
1384
+ console.log(`would update ${unitLabel(r)} ${r.installed} -> ${r.current} (${r.src})`);
1347
1385
  if (flags.force || typeof entry !== "object" || !entry.files) {
1348
1386
  const lost = editedFiles(dest, r.s, entry);
1349
1387
  if (lost.length) console.log(` --force overwrites your edits in: ${lost.join(", ")}`);
1350
1388
  continue;
1351
1389
  }
1352
- const { kept, orphaned } = mergeOne(r.cat, r.s, dest, ide, l, null, true);
1390
+ const { kept, orphaned } = mergeOne(r.cat, r.s, dest, ide, l, null, true, r.loop);
1353
1391
  for (const f of kept) console.log(` would keep your edits in ${f} and write the new version to ${f}.upstream`);
1354
1392
  for (const f of orphaned) console.log(` would keep ${f} — dropped upstream, but you edited it`);
1355
1393
  }
1356
1394
  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."));
1395
+ console.log(n ? `\n${n} skill(s) or loop(s) would update. Rerun without --dry-run to apply.` : (held ? "no version changes." : "everything up to date."));
1358
1396
  }
1359
1397
 
1360
1398
  function outdated(args) {
@@ -1368,15 +1406,15 @@ function outdated(args) {
1368
1406
  outdatedCount: stale.length,
1369
1407
  changedWithoutVersionCount: drift.length,
1370
1408
  skills: rows.map((r) => ({
1371
- name: r.s, installed: r.installed, current: r.current,
1409
+ name: r.s, loop: r.loop, installed: r.installed, current: r.current,
1372
1410
  source: r.src, outdated: !!(r.current && r.current !== r.installed),
1373
1411
  changedWithoutVersion: r.changed || [], pinned: r.pinned,
1374
1412
  })),
1375
1413
  });
1376
1414
  if (!rows.length) return console.log(`nothing installed at ${dest}`);
1377
- for (const r of stale) console.log(`${r.s}: installed ${r.installed}, current ${r.current} (${r.src})`);
1415
+ for (const r of stale) console.log(`${unitLabel(r)}: installed ${r.installed}, current ${r.current} (${r.src})`);
1378
1416
  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 ? ", …" : ""})`);
1417
+ console.log(`${unitLabel(r)}: ${r.changed.length} file(s) changed in '${r.src}' without a version bump (${r.changed.slice(0, 3).join(", ")}${r.changed.length > 3 ? ", …" : ""})`);
1380
1418
  if (drift.length)
1381
1419
  console.log(`\n${drift.length} skill(s) changed upstream with the same version. Read the change first (skilldrop diff <skill>),` +
1382
1420
  `\nthen take it with: skilldrop update --changed`);
@@ -1424,23 +1462,23 @@ function uninstall(args) {
1424
1462
  edits, an upstream change, or both. File-level always; line-level through git when it is on
1425
1463
  the PATH (git diff --no-index works outside any repo). */
1426
1464
  function diff(args) {
1427
- const s = args._[0] || die("pass an installed skill name: skilldrop diff <skill>");
1465
+ const s = args._[0] || die("pass an installed skill or loop name: skilldrop diff <name>");
1428
1466
  const { dest } = target(args.flags);
1429
1467
  const l = ledger(dest);
1430
1468
  const entry = l.data[s];
1431
1469
  if (!entry && !fs.existsSync(path.join(dest, s))) die(`'${s}' is not installed at ${dest}`);
1432
- if (entry && entry.loop) die(`'${s}' is a loop — diff compares skills`);
1470
+ const loop = isLoopEntry(entry);
1433
1471
  const src = args.flags.from || (entry ? lsrc(entry) : BUNDLED);
1434
1472
  const cat = resolveCatalog(src === BUNDLED ? undefined : src);
1435
- if (!skillExists(cat, s)) die(`'${s}' is no longer in catalog '${cat.source}'`);
1436
- const theirs = fileHashes(skillDir(cat, s)), mine = fileHashes(path.join(dest, s));
1473
+ if (!unitExists(cat, s, loop)) die(`'${s}' is no longer in catalog '${cat.source}'`);
1474
+ const theirs = unitHashes(cat, s, loop), mine = fileHashes(path.join(dest, s));
1437
1475
  const base = (entry && entry.files) || {};
1438
1476
  const changed = Object.keys(theirs).filter((f) => f in mine && mine[f] !== theirs[f]).sort();
1439
1477
  const onlyUp = Object.keys(theirs).filter((f) => !(f in mine)).sort();
1440
1478
  const onlyMine = Object.keys(mine).filter((f) => !(f in theirs)).sort();
1441
1479
  const why = (f) => !base[f] ? "" : mine[f] !== base[f] && theirs[f] !== base[f] ? " (you edited it, and so did the catalog)"
1442
1480
  : mine[f] !== base[f] ? " (your edit)" : " (catalog changed it)";
1443
- const iv = entry ? lver(entry) : "?", cv = manifestOf(cat, s).version;
1481
+ const iv = entry ? lver(entry) : "?", cv = unitMeta(cat, s, loop).version;
1444
1482
  console.log(`${s}: installed ${iv} at ${path.join(dest, s)} vs catalog '${cat.source}' ${cv}`);
1445
1483
  if (!changed.length && !onlyUp.length && !onlyMine.length) return console.log("identical.");
1446
1484
  for (const f of changed) console.log(` M ${f}${why(f)}`);
@@ -1449,7 +1487,7 @@ function diff(args) {
1449
1487
  if (args.flags.stat) return;
1450
1488
  let git = true;
1451
1489
  for (const f of changed) {
1452
- const a = path.join(skillDir(cat, s), ...f.split("/")), b = path.join(dest, s, ...f.split("/"));
1490
+ const a = unitSource(cat, s, loop, f), b = path.join(dest, s, ...f.split("/"));
1453
1491
  try {
1454
1492
  execFileSync("git", ["--no-pager", "diff", "--no-index", "--color=auto", "--src-prefix=catalog/", "--dst-prefix=installed/", a, b],
1455
1493
  { stdio: ["ignore", "inherit", "inherit"] });
@@ -1459,7 +1497,7 @@ function diff(args) {
1459
1497
  }
1460
1498
  }
1461
1499
  if (!git) console.log("\n(git not found — line-level diff needs git on the PATH; file list above)");
1462
- if (changed.length) console.log(`\nTake the catalog's version: skilldrop update --force, or install ${s} again.`);
1500
+ if (changed.length) console.log(`\nTake the catalog's version: skilldrop update --force, or install ${loop ? "--loop " : ""}${s} again.`);
1463
1501
  }
1464
1502
 
1465
1503
  /* doctor: check an install target against its ledger and report what's out of step — skills
@@ -1477,10 +1515,13 @@ function doctor(args) {
1477
1515
  const names = Object.keys(data);
1478
1516
  for (const n of names) {
1479
1517
  if (!fs.existsSync(path.join(dest, n))) add("error", `${n} is in the ledger but its folder is gone`, `skilldrop install ${n} (or uninstall ${n} to drop the record)`);
1480
- else if (!data[n].loop && !data[n].file) {
1518
+ else if (!data[n].file) {
1519
+ const loop = isLoopEntry(data[n]);
1481
1520
  const e = editedFiles(dest, n, data[n]);
1482
1521
  if (e.length) add("info", `${n}: you edited ${e.join(", ")}`, `skilldrop diff ${n}`);
1483
- if (typeof data[n] === "string" || !data[n].files) add("warn", `${n} has no file baseline (installed before 0.13)`, `skilldrop install ${n} once; update will keep your edits after that`);
1522
+ if (typeof data[n] === "string" || !data[n].files)
1523
+ add("warn", `${loop ? "loop " : ""}${n} has no file baseline (installed before ${loop ? "0.16.11" : "0.13"})`,
1524
+ `skilldrop install ${loop ? "--loop " : ""}${n} once; update will keep your edits after that`);
1484
1525
  }
1485
1526
  }
1486
1527
  if (fs.existsSync(dest)) {
@@ -1843,15 +1884,8 @@ function installLoops(args) {
1843
1884
  const l = ledger(dest);
1844
1885
  const notes = [];
1845
1886
  for (const n of names) {
1846
- const sp = loopSpec(cat, n);
1847
- const out = path.join(dest, n);
1848
- fs.mkdirSync(out, { recursive: true });
1849
- fs.copyFileSync(path.join(loopDir(cat, n), "LOOP.md"), path.join(out, "SKILL.md"));
1850
- fs.copyFileSync(path.join(loopDir(cat, n), "loop.json"), path.join(out, "loop.json"));
1851
- const note = writeWiring(ide, dest, n, sp.description);
1852
- if (note) notes.push(note);
1853
- l.data[n] = { version: sp.version || null, source: cat.source, loop: true };
1854
- console.log(`installed loop ${n} -> ${out}`);
1887
+ copyLoop(cat, n, dest, ide, l, notes);
1888
+ console.log(`installed loop ${n} -> ${path.join(dest, n)}`);
1855
1889
  }
1856
1890
  saveLedger(l);
1857
1891
  if (args.flags.local) addLocal(dest, ide, names);
@@ -5,7 +5,7 @@
5
5
  "$comment": "RFC-0028. A loop sequences existing skills; it never contains one. Skills stay in their own packs/<pack>/skills/<name> folders and stay independently installable — a loop is a named order plus the gates between stages. Enforced by validate.py with hand-rolled stdlib checks: skilldrop ships zero runtime dependencies and this file must not become a reason to add `jsonschema`.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["name", "description", "entrypoint", "kind", "stages"],
8
+ "required": ["name", "version", "description", "entrypoint", "kind", "stages"],
9
9
  "properties": {
10
10
  "$comment": { "type": "string" },
11
11
  "name": {
@@ -13,7 +13,7 @@
13
13
  "pattern": "^[a-z0-9][a-z0-9-]*$",
14
14
  "description": "Kebab-case. Must equal the folder name and the LOOP.md frontmatter name — the same triple-match skills are held to."
15
15
  },
16
- "version": { "type": "string" },
16
+ "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "description": "Bump it whenever LOOP.md or loop.json changes: `skilldrop update` compares it with the version a user installed." },
17
17
  "description": {
18
18
  "type": "string",
19
19
  "description": "Use-case-first. First half says what the loop delivers; second half states the trigger phrases an agent matches on."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.16.10",
3
+ "version": "0.16.12",
4
4
  "description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, 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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0042. One job application, start to send. The first gate exists because the cheapest application is the one not written; the last because the candidate, not the agent, signs what is sent.",
3
3
  "name": "apply",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Take one job posting and one resume to an application ready to send — an honest fit check, a tailored resume, a cover letter and the answers to the application form — with the candidate deciding whether to apply before any writing, and confirming every claim is true before anything is sent. Use when the user wants to apply for a job, says \"help me apply for this\", or wants a resume and cover letter prepared for one posting.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0028. This wrapper is docs/knowledge-work-pipeline.mmd promoted from a hand-maintained diagram into the machine-readable source that diagram is now generated from. It is a wrapper, not a loop: the generator at its centre is chosen at run time from whichever artifact the user actually wants.",
3
3
  "name": "ship-a-draft",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Take raw mess to a stakeholder-ready artifact by wrapping any skilldrop generator in the two passes either side of it — structured intake before, critique and machine-residue scrub after. Use when the user has notes, a transcript, or a ticket and wants a finished document rather than a first draft.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "wrapper",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0031. Covers the step between build (ends at merge, G2.1) and operate (starts at instrument). Its gates are G3 and G3.1: the whole number places the loop in the lifecycle (discover G0 through operate G4), and a decimal is a second gate inside the same loop (RFC-0044).",
3
3
  "name": "release",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Take merged code to live users through a planned, reversible rollout — plan the phases and the rollback, judge readiness on evidence, draft the release notes, then a human makes the go/no-go call. Use when the user has merged a change and asks how to roll it out, whether they are ready to launch, or for a go/no-go decision.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0028. The only loop whose failures are irreversible and live. Its feedback edge is real and already documented in the repo: postmortem-generator emits runbook deltas phrased as edits for runbook-generator.",
3
3
  "name": "operate",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Take a shipped service through detection, response, and learning — instrument it, write the runbook, communicate the incident, then feed the postmortem's deltas back into the runbook. Use when the user is preparing to run a service, is in an incident, or is writing a postmortem.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",