skillwiki 0.2.0-beta.7 → 0.2.0-beta.8

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/dist/cli.js CHANGED
@@ -63,7 +63,10 @@ var ExitCode = {
63
63
  INVALID_CONFIG_KEY: 26,
64
64
  CONFIG_WRITE_FAILED: 27,
65
65
  DOCTOR_HAS_WARNINGS: 28,
66
- DOCTOR_HAS_ERRORS: 29
66
+ DOCTOR_HAS_ERRORS: 29,
67
+ ARCHIVE_TARGET_NOT_FOUND: 30,
68
+ ARCHIVE_ALREADY_ARCHIVED: 31,
69
+ DRIFT_DETECTED: 32
67
70
  };
68
71
 
69
72
  // ../shared/src/json-output.ts
@@ -241,7 +244,7 @@ async function runHash(input) {
241
244
  const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
242
245
  return {
243
246
  exitCode: ExitCode.OK,
244
- result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
247
+ result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
245
248
  };
246
249
  }
247
250
 
@@ -271,7 +274,7 @@ function runFetchGuardSync(input) {
271
274
  result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
272
275
  };
273
276
  }
274
- return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
277
+ return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
275
278
  }
276
279
  function sanitizeUrl(u) {
277
280
  const clone = new URL(u.toString());
@@ -311,17 +314,18 @@ async function runValidate(input) {
311
314
  }
312
315
  const det = detectSchema(fm.data);
313
316
  if (!det.schema) {
314
- return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
317
+ return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
315
318
  }
316
319
  const parsed = SCHEMAS[det.schema].safeParse(fm.data);
317
320
  if (!parsed.success) {
318
321
  const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
319
322
  return {
320
323
  exitCode: ExitCode.INVALID_FRONTMATTER,
321
- result: ok({ schema: det.schema, valid: false, errors })
324
+ result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
325
+ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
322
326
  };
323
327
  }
324
- return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
328
+ return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
325
329
  }
326
330
 
327
331
  // src/commands/graph.ts
@@ -407,7 +411,8 @@ async function runGraphBuild(input) {
407
411
  }
408
412
  return {
409
413
  exitCode: ExitCode.OK,
410
- result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count })
414
+ result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
415
+ written: ${input.out}` })
411
416
  };
412
417
  }
413
418
  function computeAdamicAdar(adj) {
@@ -482,7 +487,8 @@ async function runOverlap(input) {
482
487
  }
483
488
  return { id, members, score };
484
489
  });
485
- return { exitCode: ExitCode.OK, result: ok({ clusters }) };
490
+ const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
491
+ return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
486
492
  }
487
493
 
488
494
  // src/utils/wiki-path.ts
@@ -668,7 +674,11 @@ async function runOrphans(input) {
668
674
  }
669
675
  }
670
676
  }
671
- return { exitCode: ExitCode.OK, result: ok({ orphans, bridges }) };
677
+ const hintLines = [];
678
+ if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
679
+ if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
680
+ if (hintLines.length === 0) hintLines.push("no orphans or bridges");
681
+ return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
672
682
  }
673
683
  function simulateRemoval(adj, removed) {
674
684
  const seen = /* @__PURE__ */ new Set();
@@ -731,13 +741,20 @@ async function runAudit(input) {
731
741
  const referenced = new Set(resolved.map((m) => m.target));
732
742
  const unused_sources = sources.filter((s) => !referenced.has(s));
733
743
  const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
744
+ const broken = resolved.filter((m) => !m.resolved);
745
+ const hintLines = [];
746
+ hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
747
+ if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
748
+ if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
749
+ if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
750
+ const humanHint = hintLines.join("\n");
734
751
  if (resolved.some((m) => !m.resolved)) {
735
- return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
752
+ return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
736
753
  }
737
754
  if (unused_sources.length > 0 || missing_from_sources.length > 0) {
738
- return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
755
+ return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
739
756
  }
740
- return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
757
+ return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
741
758
  }
742
759
  async function findVaultRoot(start) {
743
760
  let cur = start;
@@ -817,7 +834,12 @@ async function runInstall(input) {
817
834
  }
818
835
  const manifest_path = join4(input.target, "wiki-manifest.json");
819
836
  if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
820
- return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path }) };
837
+ const hintLines = [
838
+ `installed: ${installed.length}`,
839
+ input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
840
+ `manifest: ${manifest_path}`
841
+ ];
842
+ return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
821
843
  }
822
844
 
823
845
  // src/commands/path.ts
@@ -829,7 +851,7 @@ async function runPath(input) {
829
851
  home: input.home,
830
852
  explain: input.explain
831
853
  });
832
- return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
854
+ return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
833
855
  }
834
856
  const r = await resolveRuntimePath({
835
857
  flag: input.flag,
@@ -838,7 +860,7 @@ async function runPath(input) {
838
860
  explain: input.explain
839
861
  });
840
862
  if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
841
- return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {} }) };
863
+ return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
842
864
  }
843
865
 
844
866
  // src/utils/lang.ts
@@ -892,7 +914,8 @@ async function runLang(input) {
892
914
  value: resolved.value,
893
915
  source: resolved.source,
894
916
  canonical: resolved.canonical,
895
- ...chain ? { chain } : {}
917
+ ...chain ? { chain } : {},
918
+ humanHint: `${resolved.value} (via ${resolved.source})`
896
919
  })
897
920
  };
898
921
  }
@@ -1097,6 +1120,14 @@ async function runInit(input) {
1097
1120
  }
1098
1121
  }
1099
1122
  const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
1123
+ const humanHint = [
1124
+ `vault: ${target}`,
1125
+ `domain: ${domain}`,
1126
+ `lang: ${canonicalLang}`,
1127
+ `created: ${created.length}, preserved: ${preserved.length}`,
1128
+ `discovered tags: ${discovered_tags}`,
1129
+ skipEnv ? "env: skipped" : `env: ${envWritten}`
1130
+ ].join("\n");
1100
1131
  return {
1101
1132
  exitCode: ExitCode.OK,
1102
1133
  result: ok({
@@ -1109,7 +1140,8 @@ async function runInit(input) {
1109
1140
  env_written: envWritten,
1110
1141
  env_skipped: skipEnv,
1111
1142
  imported_from_hermes: importedFromHermes,
1112
- discovered_tags
1143
+ discovered_tags,
1144
+ humanHint
1113
1145
  })
1114
1146
  };
1115
1147
  }
@@ -1144,9 +1176,10 @@ async function runLinks(input) {
1144
1176
  }
1145
1177
  }
1146
1178
  if (broken.length > 0) {
1147
- return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken }) };
1179
+ return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
1180
+ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
1148
1181
  }
1149
- return { exitCode: ExitCode.OK, result: ok({ broken }) };
1182
+ return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
1150
1183
  }
1151
1184
 
1152
1185
  // src/commands/tag-audit.ts
@@ -1173,9 +1206,9 @@ async function runTagAudit(input) {
1173
1206
  }
1174
1207
  }
1175
1208
  if (violations.length > 0) {
1176
- return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
1209
+ return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
1177
1210
  }
1178
- return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
1211
+ return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
1179
1212
  }
1180
1213
 
1181
1214
  // src/commands/index-check.ts
@@ -1208,10 +1241,14 @@ async function runIndexCheck(input) {
1208
1241
  for (const [lower, orig] of indexSlugsLower) {
1209
1242
  if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
1210
1243
  }
1244
+ const hintLines = [];
1245
+ if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
1246
+ if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
1247
+ if (hintLines.length === 0) hintLines.push("index OK");
1211
1248
  if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1212
- return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
1249
+ return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1213
1250
  }
1214
- return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
1251
+ return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1215
1252
  }
1216
1253
 
1217
1254
  // src/commands/stale.ts
@@ -1251,8 +1288,8 @@ async function runStale(input) {
1251
1288
  stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
1252
1289
  }
1253
1290
  }
1254
- if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
1255
- return { exitCode: ExitCode.OK, result: ok({ stale }) };
1291
+ if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
1292
+ return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
1256
1293
  }
1257
1294
 
1258
1295
  // src/commands/pagesize.ts
@@ -1267,8 +1304,8 @@ async function runPagesize(input) {
1267
1304
  const count = body.split("\n").length;
1268
1305
  if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
1269
1306
  }
1270
- if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
1271
- return { exitCode: ExitCode.OK, result: ok({ oversized }) };
1307
+ if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
1308
+ return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
1272
1309
  }
1273
1310
 
1274
1311
  // src/commands/log-rotate.ts
@@ -1291,12 +1328,12 @@ async function runLogRotate(input) {
1291
1328
  const matches = [...logText.matchAll(ENTRY_RE)];
1292
1329
  const entries = matches.length;
1293
1330
  if (entries < input.threshold) {
1294
- return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
1331
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
1295
1332
  }
1296
1333
  if (!input.apply) {
1297
1334
  return {
1298
1335
  exitCode: ExitCode.LOG_ROTATE_NEEDED,
1299
- result: ok({ entries, threshold: input.threshold, rotated: false })
1336
+ result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
1300
1337
  };
1301
1338
  }
1302
1339
  const newestYear = matches[matches.length - 1][1];
@@ -1317,13 +1354,51 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
1317
1354
  } catch (e) {
1318
1355
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
1319
1356
  }
1320
- return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
1357
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
1358
+ }
1359
+
1360
+ // src/commands/topic-map-check.ts
1361
+ var DEFAULT_THRESHOLD = 200;
1362
+ async function runTopicMapCheck(input) {
1363
+ const threshold = input.threshold ?? DEFAULT_THRESHOLD;
1364
+ const scan = await scanVault(input.vault);
1365
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1366
+ const page_count = scan.data.typedKnowledge.length;
1367
+ const recommended = page_count >= threshold;
1368
+ return {
1369
+ exitCode: ExitCode.OK,
1370
+ result: ok({
1371
+ recommended,
1372
+ page_count,
1373
+ threshold,
1374
+ humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
1375
+ })
1376
+ };
1377
+ }
1378
+
1379
+ // src/commands/index-link-format.ts
1380
+ import { readFile as readFile11 } from "fs/promises";
1381
+ import { join as join12 } from "path";
1382
+ var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
1383
+ async function runIndexLinkFormat(input) {
1384
+ let text = "";
1385
+ try {
1386
+ text = await readFile11(join12(input.vault, "index.md"), "utf8");
1387
+ } catch {
1388
+ }
1389
+ const markdown_links = [];
1390
+ for (const [i, line] of text.split("\n").entries()) {
1391
+ if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
1392
+ }
1393
+ const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
1394
+ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1395
+ return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
1321
1396
  }
1322
1397
 
1323
1398
  // src/commands/lint.ts
1324
1399
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_drift", "tag_not_in_taxonomy"];
1325
- var WARNING_ORDER = ["index_incomplete", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1326
- var INFO_ORDER = ["bridges", "low_confidence_single_source"];
1400
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1401
+ var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
1327
1402
  async function runLint(input) {
1328
1403
  const buckets = {};
1329
1404
  const links = await runLinks({ vault: input.vault });
@@ -1343,6 +1418,10 @@ async function runLint(input) {
1343
1418
  ghost_entries: idx.result.data.ghost_entries
1344
1419
  }];
1345
1420
  }
1421
+ const linkFmt = await runIndexLinkFormat({ vault: input.vault });
1422
+ if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
1423
+ buckets.index_link_format = linkFmt.result.data.markdown_links;
1424
+ }
1346
1425
  const stale = await runStale({ vault: input.vault, days: input.days });
1347
1426
  if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
1348
1427
  const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
@@ -1356,6 +1435,10 @@ async function runLint(input) {
1356
1435
  if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
1357
1436
  if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
1358
1437
  }
1438
+ const topicMap = await runTopicMapCheck({ vault: input.vault });
1439
+ if (topicMap.result.ok && topicMap.result.data.recommended) {
1440
+ buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
1441
+ }
1359
1442
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1360
1443
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1361
1444
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -1367,25 +1450,35 @@ async function runLint(input) {
1367
1450
  let exitCode = ExitCode.OK;
1368
1451
  if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
1369
1452
  else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
1453
+ const hintLines = [];
1454
+ if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
1455
+ if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
1456
+ if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
1457
+ const allBuckets = [...errorOut, ...warningOut, ...infoOut];
1458
+ for (const b of allBuckets) {
1459
+ hintLines.push(` ${b.kind}: ${b.items.length}`);
1460
+ }
1461
+ if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
1370
1462
  return {
1371
1463
  exitCode,
1372
1464
  result: ok({
1373
1465
  vault: { path: input.vault, source: input.source ?? "resolved" },
1374
1466
  summary,
1375
- by_severity: { error: errorOut, warning: warningOut, info: infoOut }
1467
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
1468
+ humanHint: hintLines.join("\n")
1376
1469
  })
1377
1470
  };
1378
1471
  }
1379
1472
 
1380
1473
  // src/commands/config.ts
1381
- import { readFile as readFile11 } from "fs/promises";
1474
+ import { readFile as readFile12 } from "fs/promises";
1382
1475
  import { existsSync } from "fs";
1383
- import { join as join12 } from "path";
1476
+ import { join as join13 } from "path";
1384
1477
  function validateKey(key) {
1385
1478
  return CONFIG_KEYS.includes(key);
1386
1479
  }
1387
1480
  function configPath(home) {
1388
- return join12(home, ".skillwiki", ".env");
1481
+ return join13(home, ".skillwiki", ".env");
1389
1482
  }
1390
1483
  async function runConfigGet(input) {
1391
1484
  if (!validateKey(input.key)) {
@@ -1403,7 +1496,7 @@ async function runConfigSet(input) {
1403
1496
  try {
1404
1497
  let originalContent;
1405
1498
  try {
1406
- originalContent = await readFile11(filePath, "utf8");
1499
+ originalContent = await readFile12(filePath, "utf8");
1407
1500
  } catch {
1408
1501
  }
1409
1502
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -1426,7 +1519,7 @@ async function runConfigPath(input) {
1426
1519
 
1427
1520
  // src/commands/doctor.ts
1428
1521
  import { existsSync as existsSync2, readdirSync, statSync } from "fs";
1429
- import { join as join13 } from "path";
1522
+ import { join as join14 } from "path";
1430
1523
  import { execSync } from "child_process";
1431
1524
  function check(status, id, label, detail) {
1432
1525
  return { id, label, status, detail };
@@ -1482,9 +1575,9 @@ function checkVaultStructure(resolvedPath) {
1482
1575
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1483
1576
  }
1484
1577
  const missing = [];
1485
- if (!existsSync2(join13(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1578
+ if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1486
1579
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
1487
- if (!existsSync2(join13(resolvedPath, dir))) missing.push(dir + "/");
1580
+ if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
1488
1581
  }
1489
1582
  if (missing.length === 0) {
1490
1583
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -1492,7 +1585,7 @@ function checkVaultStructure(resolvedPath) {
1492
1585
  return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1493
1586
  }
1494
1587
  function checkSkillsInstalled(home) {
1495
- const skillsDir = join13(home, ".claude", "skills");
1588
+ const skillsDir = join14(home, ".claude", "skills");
1496
1589
  if (!existsSync2(skillsDir)) {
1497
1590
  return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1498
1591
  }
@@ -1512,9 +1605,9 @@ function findSkillMd(dir) {
1512
1605
  }
1513
1606
  for (const entry of entries) {
1514
1607
  if (entry.isFile() && entry.name === "SKILL.md") {
1515
- results.push(join13(dir, entry.name));
1608
+ results.push(join14(dir, entry.name));
1516
1609
  } else if (entry.isDirectory()) {
1517
- results.push(...findSkillMd(join13(dir, entry.name)));
1610
+ results.push(...findSkillMd(join14(dir, entry.name)));
1518
1611
  }
1519
1612
  }
1520
1613
  return results;
@@ -1552,6 +1645,123 @@ async function runDoctor(input) {
1552
1645
  return { exitCode, result: ok({ checks, summary, humanHint }) };
1553
1646
  }
1554
1647
 
1648
+ // src/commands/archive.ts
1649
+ import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
1650
+ import { join as join15, dirname as dirname6 } from "path";
1651
+ async function runArchive(input) {
1652
+ const scan = await scanVault(input.vault);
1653
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1654
+ let relPath;
1655
+ if (input.page.includes("/")) {
1656
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
1657
+ } else {
1658
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
1659
+ }
1660
+ if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1661
+ if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1662
+ const archivePath = join15("_archive", relPath);
1663
+ await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
1664
+ let indexUpdated = false;
1665
+ const indexPath = join15(input.vault, "index.md");
1666
+ try {
1667
+ const idx = await readFile13(indexPath, "utf8");
1668
+ const slug = relPath.replace(/\.md$/, "").split("/").pop();
1669
+ const originalLines = idx.split("\n");
1670
+ const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1671
+ if (filtered.length !== originalLines.length) {
1672
+ await writeFile6(indexPath, filtered.join("\n"), "utf8");
1673
+ indexUpdated = true;
1674
+ }
1675
+ } catch (e) {
1676
+ if (e?.code !== "ENOENT") throw e;
1677
+ }
1678
+ await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
1679
+ return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
1680
+ }
1681
+
1682
+ // src/commands/drift.ts
1683
+ import { createHash as createHash2 } from "crypto";
1684
+
1685
+ // src/utils/fetch.ts
1686
+ async function controlledFetch(url, opts) {
1687
+ let current = url;
1688
+ for (let hop = 0; hop <= opts.maxRedirects; hop++) {
1689
+ const guard = runFetchGuardSync({ url: current });
1690
+ if (!guard.result.ok) return guard.result;
1691
+ const ctrl = new AbortController();
1692
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
1693
+ let res;
1694
+ try {
1695
+ res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
1696
+ } catch (e) {
1697
+ clearTimeout(timer);
1698
+ if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
1699
+ return err("FETCH_FAILED", { message: String(e) });
1700
+ }
1701
+ clearTimeout(timer);
1702
+ if (res.status >= 300 && res.status < 400) {
1703
+ const loc = res.headers.get("location");
1704
+ if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
1705
+ current = new URL(loc, current).toString();
1706
+ continue;
1707
+ }
1708
+ const declared = Number(res.headers.get("content-length") ?? "0");
1709
+ if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
1710
+ const buf = new Uint8Array(await res.arrayBuffer());
1711
+ if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
1712
+ return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
1713
+ }
1714
+ return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
1715
+ }
1716
+
1717
+ // src/commands/drift.ts
1718
+ var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
1719
+ async function runDrift(input) {
1720
+ const doFetch = input.fetchFn ?? controlledFetch;
1721
+ const scan = await scanVault(input.vault);
1722
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1723
+ const results = [];
1724
+ for (const raw of scan.data.raw) {
1725
+ const fm = extractFrontmatter(await readPage(raw));
1726
+ if (!fm.ok) continue;
1727
+ const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
1728
+ const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1729
+ if (!sourceUrl || !storedHash) continue;
1730
+ const resp = await doFetch(sourceUrl, FETCH_OPTS);
1731
+ if (!resp.ok) {
1732
+ results.push({
1733
+ raw_path: raw.relPath,
1734
+ source_url: sourceUrl,
1735
+ stored_sha256: storedHash,
1736
+ current_sha256: null,
1737
+ status: "fetch_failed",
1738
+ fetch_error: resp.error
1739
+ });
1740
+ continue;
1741
+ }
1742
+ const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1743
+ const drifted2 = currentHash !== storedHash;
1744
+ results.push({
1745
+ raw_path: raw.relPath,
1746
+ source_url: sourceUrl,
1747
+ stored_sha256: storedHash,
1748
+ current_sha256: currentHash,
1749
+ status: drifted2 ? "drifted" : "unchanged"
1750
+ });
1751
+ }
1752
+ const drifted = results.filter((r) => r.status === "drifted");
1753
+ const fetchFailed = results.filter((r) => r.status === "fetch_failed");
1754
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
1755
+ const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
1756
+ const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
1757
+ if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
1758
+ if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
1759
+ return {
1760
+ exitCode,
1761
+ result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
1762
+ };
1763
+ }
1764
+
1555
1765
  // src/cli.ts
1556
1766
  var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
1557
1767
  var program = new Command();
@@ -1672,6 +1882,16 @@ program.command("doctor").description("diagnose skillwiki setup issues").action(
1672
1882
  envValue: process.env.WIKI_PATH,
1673
1883
  argv: process.argv
1674
1884
  })));
1885
+ program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
1886
+ const v = await resolveVaultArg(vault);
1887
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1888
+ else emit(await runArchive({ vault: v.vault, page }));
1889
+ });
1890
+ program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
1891
+ const v = await resolveVaultArg(vault);
1892
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1893
+ else emit(await runDrift({ vault: v.vault }));
1894
+ });
1675
1895
  program.parseAsync(process.argv).catch((e) => {
1676
1896
  process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
1677
1897
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.8",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.8",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -12,10 +12,32 @@ description: 2-step distillation (E4) — analyze project compound entry, then g
12
12
  Standard four + project context.
13
13
 
14
14
  ## Steps (E4 — 2-step pattern)
15
- 1. **Step 1 — Analyze.** Read the source compound entry + linked work items. Output a candidate concept outline. STOP if no clear universal pattern is found — surface the reasoning instead of forcing a page.
16
- 2. **Step 2 — Generate.** Compose the vault concept page with `provenance: project` and `provenance_projects: ["[[slug]]"]`. Validate with `npx skillwiki validate`.
17
- 3. **Backlink.** Set `promoted_to: "[[concept-slug]]"` on the source compound entry.
18
- 4. **Apply writes in order.** Vault concept page → backlink update → project `log.md` vault `index.md` vault `log.md`.
15
+
16
+ ### Source selection
17
+
18
+ Check `projects/{slug}/compound/` first. If empty, fall back to retro
19
+ entries in vault `log.md` (lines matching `## [YYYY-MM-DD] retro`).
20
+
21
+ When reading retros as source material:
22
+ - Collect all retros for the project, focusing on entries with
23
+ `Generalize?: yes`.
24
+ - Group by recurring theme (≥2 occurrences across cycles).
25
+ - Each group becomes a candidate concept outline.
26
+
27
+ 1. **Step 1 — Analyze.** Read the source compound entry + linked work
28
+ items (or retro groups from log.md). Output a candidate concept
29
+ outline. STOP if no clear universal pattern is found — surface the
30
+ reasoning instead of forcing a page.
31
+ 2. **Step 2 — Generate.** Compose the vault concept page with
32
+ `provenance: project` and
33
+ `provenance_projects: ["[[slug]]"]`. Validate with
34
+ `npx skillwiki validate`.
35
+ 3. **Backlink.** Set `promoted_to: "[[concept-slug]]"` on the source
36
+ compound entry. For retro-sourced distillation, skip backlink (log.md
37
+ entries are append-only) and instead add `sources:` citing the vault
38
+ log with date range.
39
+ 4. **Apply writes in order.** Vault concept page → backlink update →
40
+ project `log.md` → vault `index.md` → vault `log.md`.
19
41
 
20
42
  ## Stop conditions
21
43
  - No clear universal pattern.
@@ -20,6 +20,25 @@ Standard four + project context (project README, last ~5 work logs).
20
20
  5. Manage status transitions: `planned` → `in-progress` → `completed` (set `completed:` date) or `abandoned`.
21
21
  6. Append vault `log.md` entry on creation and on each status transition.
22
22
 
23
+ ## Redirect Output
24
+
25
+ After step 3 (output path override), emit redirect paths for the active PRD skill:
26
+
27
+ > Work item created: projects/{slug}/work/YYYY-MM-DD-{work-slug}/
28
+ >
29
+ > Redirect paths for PRD skills:
30
+ > spec → <vault-root>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/spec.md
31
+ > plan → <vault-root>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/plan.md
32
+ >
33
+ > Pass these paths to your PRD skill (superpowers:brainstorming, superpowers:writing-plans,
34
+ > CodeStable, or any other). Files land in the vault natively — no separate ingest needed.
35
+
36
+ Rules:
37
+ - Emit redirect paths as the first output after folder creation, before any PRD skill runs.
38
+ - Resolve `<vault-root>` via `skillwiki path` (never hardcode).
39
+ - proj-work does NOT invoke any PRD skill — it provides paths only.
40
+ - If the PRD skill cannot accept custom save paths, fall back to manual `wiki-ingest`.
41
+
23
42
  ## Stop conditions
24
43
  - `validate` non-zero.
25
44
  - Conflicting work folder name.
@@ -20,6 +20,9 @@ Invoke a skillwiki skill when the user:
20
20
  - Wants a health check or lint on their vault
21
21
  - Mentions crystallizing a session into a note
22
22
  - Talks about project workspaces, ADRs, or distillation
23
+ - Wants to archive or clean up old vault pages
24
+ - Needs to detect source drift or re-ingest updated content
25
+ - Has a spec/plan in a non-skillwiki format (CodeStable, RFC, AIDE)
23
26
  - Asks about their skillwiki configuration or setup health
24
27
 
25
28
  ## Skill Map
@@ -32,6 +35,9 @@ Invoke a skillwiki skill when the user:
32
35
  | `wiki-lint` | Vault health check (stale pages, oversized pages, log rotation) |
33
36
  | `wiki-crystallize` | Distill the current working session into a typed-knowledge page |
34
37
  | `wiki-audit` | Verify raw provenance references and source frontmatter integrity |
38
+ | `wiki-archive` | Archive a typed-knowledge page — move to `_archive/`, remove from index |
39
+ | `wiki-reingest` | Detect drift in raw sources (sha256 comparison) and re-ingest updated content |
40
+ | `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) into vault pages |
35
41
  | `proj-init` | Bootstrap a project workspace (README, requirements, architecture) |
36
42
  | `proj-work` | Open or run a work item under a project's work/ directory |
37
43
  | `proj-distill` | Distill project compound entries into vault concept pages |
@@ -41,7 +47,7 @@ Invoke a skillwiki skill when the user:
41
47
 
42
48
  All skills are backed by the `skillwiki` CLI — a deterministic tool with no LLM calls. It handles path resolution, config management, validation, and linting. Skills invoke it via Bash for the mechanical parts and use Claude for the creative parts.
43
49
 
44
- Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`.
50
+ Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`, `archive`, `drift`.
45
51
 
46
52
  Run `skillwiki doctor` to diagnose setup issues. Run `skillwiki config list` to see current configuration.
47
53
 
@@ -55,3 +61,5 @@ Run `skillwiki doctor` to diagnose setup issues. Run `skillwiki config list` to
55
61
  6. **Audit** (`wiki-audit`) — verify source integrity
56
62
 
57
63
  For longer-running project work, use `proj-init` → `proj-work` → `proj-distill` / `proj-decide`.
64
+
65
+ Maintenance: **Archive** (`wiki-archive`) superseded pages, **Drift** (`wiki-reingest`) to detect stale sources, **Adapter** (`wiki-adapter-prd`) for foreign PRD format ingestion.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: wiki-adapter-prd
3
+ description: Map foreign PRD formats (CodeStable, RFCs, structured markdown) into skillwiki raw + typed-knowledge pages.
4
+ ---
5
+
6
+ # wiki-adapter-prd
7
+
8
+ ## When This Skill Activates
9
+
10
+ - User provides a document or URL in a non-skillwiki PRD format and wants it captured in the vault.
11
+ - User mentions CodeStable, RFC, AIDE, or another structured design document format.
12
+ - A foreign spec/plan needs to be normalized into the vault's raw + concept structure.
13
+
14
+ ## Output language
15
+
16
+ Run `skillwiki lang` at the start. Generate page prose in the resolved language. Frontmatter keys, file names, and structural markers stay English.
17
+
18
+ ## Pre-orientation reads
19
+
20
+ Standard four reads (SCHEMA, index, log, project context if applicable).
21
+
22
+ ## Recognized PRD Formats
23
+
24
+ | Format | Structural cues |
25
+ |--------|----------------|
26
+ | CodeStable | `REQ-NNN` requirement IDs, `## Requirements` / `## Architecture` headers |
27
+ | RFC | `## Motivation` / `## Proposal` / `## Drawbacks` headers |
28
+ | AIDE directives | Specific YAML frontmatter keys (`aide-*`) |
29
+ | Hermes spec | `N1`–`N18` normative requirement markers |
30
+ | Generic structured | Clear `##` section hierarchy with requirements, decisions, or designs |
31
+
32
+ If the format is unrecognized, treat as generic structured markdown and map by section hierarchy.
33
+
34
+ ## Mapping Strategy
35
+
36
+ ### Raw capture (verbatim)
37
+
38
+ - Write the full source document to `raw/articles/<slug>.md` with RawSourceSchema frontmatter (`sha256`, `source_url`, `ingested`, `ingested_by: "wiki-ingest"`).
39
+ - If the source is a URL, run `skillwiki fetch-guard <url>` first.
40
+ - Run `skillwiki hash <raw-file>` to compute sha256.
41
+
42
+ ### Knowledge extraction
43
+
44
+ Map source sections to typed-knowledge pages:
45
+
46
+ | Source section | Target type | Notes |
47
+ |----------------|-------------|-------|
48
+ | Requirements list | `concepts/` or `entities/` | Each major requirement becomes its own page or a section in a compound page |
49
+ | Architecture decisions | `concepts/` | Map to concept pages with `tags: [architecture]` |
50
+ | Motivation / context | `entities/` | Capture as entity pages describing the system or component |
51
+ | Trade-offs / comparisons | `comparisons/` | Create comparison pages when the source weighs alternatives |
52
+ | Action items / next steps | Skip | Not knowledge — track in project work items instead |
53
+
54
+ ### Cross-reference handling
55
+
56
+ - Requirement IDs (`REQ-NNN`, `N1`–`N18`) → preserve as frontmatter tags or inline references.
57
+ - Internal links within the source → convert to `[[wikilinks]]` where corresponding pages exist.
58
+ - External URLs → keep as-is in body text.
59
+
60
+ ## Steps
61
+
62
+ 0. Resolve vault and language: `skillwiki path` and `skillwiki lang`.
63
+ 1. Classify the input format using the structural cues above.
64
+ 2. If URL source: run `skillwiki fetch-guard <url>`, then fetch.
65
+ 3. Write raw capture: frontmatter + full body → `raw/articles/<slug>.md`.
66
+ 4. Run `skillwiki hash <raw-file>`, embed sha256.
67
+ 5. Generate typed-knowledge pages following the mapping strategy.
68
+ 6. For each page: run `skillwiki validate <page>`. If any fails, STOP.
69
+ 7. Write pages, then update `index.md` and `log.md`.
70
+
71
+ ## Provenance defaults
72
+
73
+ - `provenance: research` (external PRD sources).
74
+ - `sources: ["^[raw/articles/<slug>.md]"]` on every generated page.
75
+
76
+ ## Stop conditions
77
+
78
+ - `fetch-guard` non-zero.
79
+ - `validate` non-zero on any page.
80
+ - sha256 already exists for the same source (skip — already ingested).
81
+
82
+ ## Forbidden
83
+
84
+ - Skipping `fetch-guard` for URL sources.
85
+ - Writing index/log before all pages validate.
86
+ - Modifying existing raw files (N9).
87
+ - Auto-generating pages for action items, timelines, or process steps — those are project management, not knowledge.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: wiki-archive
3
+ description: Archive a superseded typed-knowledge page. Moves page to _archive/, removes from index.md, logs the action.
4
+ ---
5
+
6
+ # wiki-archive
7
+
8
+ ## When This Skill Activates
9
+
10
+ - User wants to retire, supersede, or remove a typed-knowledge page from active use.
11
+ - A page has been replaced by a newer version and should be kept for reference but excluded from lint and queries.
12
+
13
+ ## Output language
14
+
15
+ Run `skillwiki lang` at the start. Generate log entries in the resolved language.
16
+
17
+ ## Pre-orientation reads
18
+
19
+ Standard four reads (SCHEMA, index, log, project context if applicable).
20
+
21
+ ## Steps
22
+
23
+ 0. Resolve vault: `skillwiki path` and `skillwiki lang`.
24
+ 1. Identify the target page. Confirm with the user which page to archive (show full relPath).
25
+ 2. Run `skillwiki archive <page> [vault]`. Read the JSON output.
26
+ 3. Verify with `skillwiki index-check [vault]` — confirm no ghost entries remain.
27
+ 4. Append a `log.md` entry: `## [{date}] archive | {relPath} → _archive/{subdir}/`.
28
+
29
+ ## Reversibility
30
+
31
+ Archiving is reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. No data is deleted.
32
+
33
+ ## Stop conditions
34
+
35
+ - `skillwiki archive` returns non-zero exit code (page not found, already archived, invalid vault).
36
+ - User declines to proceed.
37
+
38
+ ## Forbidden
39
+
40
+ - Archiving `raw/` files (N9 — raw is immutable).
41
+ - Archiving without user confirmation.
42
+ - Deleting files (archive moves, never deletes).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wiki-ingest
3
- description: Convert URLs, files, or pasted text into typed-knowledge pages with raw provenance. Single-pass v1.
3
+ description: Convert URLs, files, or pasted text into typed-knowledge pages with raw provenance. Supports single and batch mode.
4
4
  ---
5
5
 
6
6
  # wiki-ingest
@@ -45,3 +45,14 @@ Run `skillwiki lang` at the start. Generate page-body prose, narrative sections,
45
45
  - Skipping `fetch-guard`.
46
46
  - Updating `index.md` or `log.md` before all pages validate.
47
47
  - Modifying any existing file in `raw/`.
48
+
49
+ ## Batch Mode
50
+
51
+ When the user provides multiple sources (a directory of files, a list of URLs, or a multi-document input):
52
+
53
+ 1. **Loop per source.** Execute steps 1–5 for each source individually (guard → fetch → hash → generate → validate).
54
+ 2. **Accumulate, don't write yet.** Collect all raw files and pages in memory. Do not write `index.md` or `log.md` until every source has validated.
55
+ 3. **Fail fast.** If any page fails validation, STOP. Report all failures. Do not write index/log for any source.
56
+ 4. **Deduplication.** Before writing each raw file, check `sha256` against existing vault raw sources. Skip sources whose content is already present.
57
+ 5. **Single index/log update.** After all sources validate, write all raw files and pages, then update `index.md` and `log.md` once.
58
+ 6. **Progress.** After each source completes validation, report progress (e.g., "Validated 3/10 sources").
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: wiki-reingest
3
+ description: Detect and act on source drift. Runs skillwiki drift, reviews changes, archives old raw + ingests new content.
4
+ ---
5
+
6
+ # wiki-reingest
7
+
8
+ ## When This Skill Activates
9
+
10
+ - User wants to check if any vault sources have changed since ingestion.
11
+ - Periodic drift check during lint or maintenance cycles.
12
+ - User explicitly asks to re-ingest a specific source.
13
+
14
+ ## Output language
15
+
16
+ Run `skillwiki lang` at the start. Generate log entries in the resolved language.
17
+
18
+ ## Pre-orientation reads
19
+
20
+ Standard four reads (SCHEMA, index, log, project context if applicable).
21
+
22
+ ## Steps
23
+
24
+ 0. Resolve vault: `skillwiki path` and `skillwiki lang`.
25
+ 1. Run `skillwiki drift [vault]`. Read the JSON output.
26
+ 2. Present findings grouped by status:
27
+ - **drifted:** Source content has changed. Show stored vs current sha256.
28
+ - **fetch_failed:** Could not re-fetch. Show error details.
29
+ - **unchanged:** No action needed.
30
+ 3. For each drifted source, ask the user: archive old + ingest new, or skip?
31
+ 4. If the user approves re-ingest for a source:
32
+ a. Run `skillwiki archive <raw-path>` to archive the old raw file.
33
+ b. Follow the `wiki-ingest` skill to ingest the updated content as a new raw file.
34
+ c. Update any concept/entity pages that cite the old source to reference the new one.
35
+ 5. Append a `log.md` entry summarizing: scanned, drifted, re-ingested, skipped.
36
+
37
+ ## N9 Compliance
38
+
39
+ Raw files are immutable (N9). Re-ingest never modifies an existing raw file. Instead:
40
+ - Archive the old raw file (moves to `_archive/raw/`).
41
+ - Create a new raw file with updated content and new sha256.
42
+ - This preserves full provenance history.
43
+
44
+ ## Stop conditions
45
+
46
+ - `skillwiki drift` returns non-zero exit code other than DRIFT_DETECTED.
47
+ - User declines all re-ingest actions.
48
+ - No raw sources have `source_url` (nothing to check).
49
+
50
+ ## Forbidden
51
+
52
+ - Modifying files in `raw/` directly (N9).
53
+ - Re-ingesting without user approval for each drifted source.
54
+ - Skipping the drift check and assuming sources have changed.
@@ -51,3 +51,19 @@ Rule: every tag on every page MUST appear in this taxonomy. Add new tags here fi
51
51
  - Wikilinks in YAML: quoted, `"[[name]]"`. Body wikilinks: unquoted `[[name]]`.
52
52
  - Citations in body: `^[raw/...]` markers; every entry in `sources:` MUST appear in body.
53
53
  - sha256 in `raw/` frontmatter is computed by `skillwiki hash` over body bytes after closing `---`.
54
+
55
+ ## Obsidian Integration
56
+
57
+ - **Attachment folder:** `raw/assets/` — binary assets (images, diagrams) live here.
58
+ Set Obsidian's "Attachment folder path" to `raw/assets` for automatic filing.
59
+ - **Dataview queries** (read-only; do not replace index.md):
60
+
61
+ ```dataview
62
+ LIST WHERE type = "concept" AND contains(tags, "architecture")
63
+ ```
64
+
65
+ ```dataview
66
+ TABLE updated, length(sources) AS sources
67
+ WHERE file.folder = "concepts"
68
+ SORT updated DESC
69
+ ```