skillwiki 0.9.22 → 0.9.23

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
@@ -3525,6 +3525,7 @@ function buildCliSurface() {
3525
3525
  memoryCmd2.command("topics").option("--project <slug>").option("--limit <n>").option("--wiki <name>");
3526
3526
  memoryCmd2.command("index").requiredOption("--project <slug>").option("--check").option("--if-stale").option("--wiki <name>");
3527
3527
  memoryCmd2.command("recall").requiredOption("--project <slug>").requiredOption("--topic <slug>").option("--scope <scope>").option("--limit <n>").option("--wiki <name>");
3528
+ memoryCmd2.command("review").requiredOption("--project <slug>").option("--dry-run").option("--wiki <name>");
3528
3529
  memoryCmd2.command("import").requiredOption("--from <path>").requiredOption("--project <slug>").option("--dry-run").option("--apply").option("--max-bytes <n>").option("--wiki <name>");
3529
3530
  const fleetCmd2 = program2.commands.find((c) => c.name() === "fleet");
3530
3531
  fleetCmd2.command("validate");
@@ -9138,6 +9139,41 @@ async function runMemoryRecall(input) {
9138
9139
  })
9139
9140
  };
9140
9141
  }
9142
+ async function runMemoryReview(input) {
9143
+ const scan = await scanVault(input.vault);
9144
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
9145
+ const state = await buildMemoryIndexState(scan.data.allMarkdown, input.project);
9146
+ const checked = await checkMemoryIndex(input.vault, input.project, state);
9147
+ if (!checked.ok) {
9148
+ return {
9149
+ exitCode: checked.error.exitCode,
9150
+ result: checked.error.result
9151
+ };
9152
+ }
9153
+ const findings = buildMemoryReviewFindings({
9154
+ project: input.project,
9155
+ status: checked.data,
9156
+ indexState: state,
9157
+ reviewPages: state.reviewPages
9158
+ });
9159
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
9160
+ return {
9161
+ exitCode: ExitCode.OK,
9162
+ result: ok({
9163
+ project: input.project,
9164
+ generated_at: generatedAt,
9165
+ cache: {
9166
+ present: checked.data.cache_present,
9167
+ ...checked.data.generated_at ? { generated_at: checked.data.generated_at } : {},
9168
+ stale: checked.data.stale,
9169
+ source_hash_mismatches: checked.data.drift.changed_sources
9170
+ },
9171
+ findings,
9172
+ files_written: [],
9173
+ humanHint: renderMemoryReviewHint(input.project, checked.data, findings)
9174
+ })
9175
+ };
9176
+ }
9141
9177
  async function runMemoryImport(input) {
9142
9178
  const scan = await scanVault(input.vault);
9143
9179
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -9197,14 +9233,18 @@ async function buildMemoryIndexState(pages, project) {
9197
9233
  const warnings = [];
9198
9234
  const sourcePages = dedupePages(pages);
9199
9235
  const sources = [];
9236
+ const reviewPages = [];
9200
9237
  for (const page of sourcePages) {
9201
- const source = await readMemorySource(page, project, warnings);
9202
- if (source) sources.push(source);
9238
+ const memoryPage = await readMemoryPage(page, project, warnings);
9239
+ if (!memoryPage) continue;
9240
+ sources.push(memoryPage.source);
9241
+ if (memoryPage.reviewPage) reviewPages.push(memoryPage.reviewPage);
9203
9242
  }
9204
9243
  return {
9205
9244
  sources,
9206
9245
  topics: buildTopics(project, sources),
9207
- warnings
9246
+ warnings,
9247
+ reviewPages
9208
9248
  };
9209
9249
  }
9210
9250
  async function checkMemoryIndex(vault, project, current) {
@@ -9290,6 +9330,175 @@ function emptyMemoryIndexDrift() {
9290
9330
  changed_sources: []
9291
9331
  };
9292
9332
  }
9333
+ async function readMemoryPage(page, project, warnings) {
9334
+ const text = await readPage(page);
9335
+ const fm = extractFrontmatter(text);
9336
+ if (!fm.ok) return null;
9337
+ const rawTopics = typeof fm.data.memory_topics === "string" ? [fm.data.memory_topics] : stringArray(fm.data.memory_topics);
9338
+ if (rawTopics.length === 0) return null;
9339
+ const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
9340
+ if (topics.length === 0) return null;
9341
+ const scope = stringField2(fm.data.memory_scope);
9342
+ const projects = memoryProjects(page.relPath, fm.data);
9343
+ if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
9344
+ const privacy = stringField2(fm.data.memory_privacy) || "local";
9345
+ if (privacy === "secret-blocked") {
9346
+ warnings.push(`${page.relPath}: skipped secret-blocked memory`);
9347
+ return null;
9348
+ }
9349
+ const status = stringField2(fm.data.memory_status) || "active";
9350
+ if (status === "archived" || status === "rejected") return null;
9351
+ const metadataGaps = [];
9352
+ if (!scope) metadataGaps.push("memory_scope");
9353
+ if (!stringField2(fm.data.memory_policy)) metadataGaps.push("memory_policy");
9354
+ if (!stringField2(fm.data.memory_privacy)) metadataGaps.push("memory_privacy");
9355
+ if (!stringField2(fm.data.memory_status)) metadataGaps.push("memory_status");
9356
+ if (!stringField2(fm.data.last_seen) && !stringField2(fm.data.updated) && !stringField2(fm.data.ingested)) {
9357
+ metadataGaps.push("freshness");
9358
+ }
9359
+ const split = splitFrontmatter(text);
9360
+ const body = split.ok ? split.data.body : text;
9361
+ const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
9362
+ const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
9363
+ return {
9364
+ source: {
9365
+ path: page.relPath,
9366
+ title,
9367
+ summary: summarize2(body),
9368
+ updated,
9369
+ hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
9370
+ topics,
9371
+ project,
9372
+ ...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
9373
+ ...scope ? { memory_scope: scope } : {},
9374
+ ...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
9375
+ memory_privacy: privacy,
9376
+ memory_status: status
9377
+ },
9378
+ ...metadataGaps.length > 0 ? {
9379
+ reviewPage: {
9380
+ path: page.relPath,
9381
+ metadataGaps
9382
+ }
9383
+ } : {}
9384
+ };
9385
+ }
9386
+ function buildMemoryReviewFindings(input) {
9387
+ const findings = [];
9388
+ if (!input.status.cache_present) {
9389
+ findings.push({
9390
+ id: `cache_missing:${input.project}`,
9391
+ severity: "warn",
9392
+ kind: "cache_missing",
9393
+ message: `memory cache for ${input.project} is missing`,
9394
+ suggested_action: `Run skillwiki memory index --project ${input.project} to build the local cache.`
9395
+ });
9396
+ } else if (input.status.stale) {
9397
+ findings.push({
9398
+ id: `cache_stale:${input.project}`,
9399
+ severity: "warn",
9400
+ kind: "cache_stale",
9401
+ message: `memory cache for ${input.project} is stale: ${input.status.drift.missing_sources.length} missing, ${input.status.drift.removed_sources.length} removed, ${input.status.drift.changed_sources.length} changed source(s)`,
9402
+ suggested_action: `Run skillwiki memory index --project ${input.project} or --if-stale to refresh the local cache.`
9403
+ });
9404
+ }
9405
+ for (const path of input.status.drift.changed_sources) {
9406
+ findings.push({
9407
+ id: `source_hash_drift:${path}`,
9408
+ severity: "warn",
9409
+ kind: "source_hash_drift",
9410
+ path,
9411
+ message: `${path} changed since the last cache build`,
9412
+ suggested_action: `Refresh the cache for ${input.project} and review whether the source summary still matches.`
9413
+ });
9414
+ }
9415
+ for (const warning of input.indexState.warnings) {
9416
+ const parsed = parseInvalidTopicWarning(warning);
9417
+ if (!parsed) continue;
9418
+ findings.push({
9419
+ id: `invalid_topic_slug:${parsed.path}:${parsed.topic}`,
9420
+ severity: "warn",
9421
+ kind: "invalid_topic_slug",
9422
+ path: parsed.path,
9423
+ topic: parsed.topic,
9424
+ message: `${parsed.path} declares invalid memory topic slug ${parsed.topic}`,
9425
+ suggested_action: "Normalize the topic slug to lowercase kebab-case so memory index and review can include it."
9426
+ });
9427
+ }
9428
+ for (const page of input.reviewPages) {
9429
+ if (page.metadataGaps.length === 0) continue;
9430
+ findings.push({
9431
+ id: `metadata_gap:${page.path}`,
9432
+ severity: "warn",
9433
+ kind: "metadata_gap",
9434
+ path: page.path,
9435
+ message: `${page.path} is missing explicit memory metadata: ${page.metadataGaps.join(", ")}`,
9436
+ suggested_action: "Add the missing frontmatter fields before relying on this source for cross-device memory review."
9437
+ });
9438
+ }
9439
+ const byTopic = groupSourcesByTopic(input.indexState.sources);
9440
+ for (const [topic, sources] of byTopic.entries()) {
9441
+ if (sources.length === 1) {
9442
+ findings.push({
9443
+ id: `sparse_topic:${topic}`,
9444
+ severity: "info",
9445
+ kind: "sparse_topic",
9446
+ topic,
9447
+ ...sources[0]?.path ? { path: sources[0].path } : {},
9448
+ message: `topic ${topic} has only one active memory source`,
9449
+ suggested_action: "Decide whether the topic needs more coverage or should stay intentionally narrow."
9450
+ });
9451
+ }
9452
+ const hasProjectSource = sources.some((source) => isProjectMemorySource(source, input.project));
9453
+ const hasNonProjectScope = sources.some((source) => source.memory_scope === "global" || source.memory_scope === "cross-agent");
9454
+ if (hasProjectSource && hasNonProjectScope) {
9455
+ findings.push({
9456
+ id: `scope_gap:${topic}`,
9457
+ severity: "info",
9458
+ kind: "scope_gap",
9459
+ topic,
9460
+ message: `topic ${topic} mixes project and non-project memory scopes`,
9461
+ suggested_action: `Use skillwiki memory recall --project ${input.project} --topic ${topic} --scope <project|global|cross-agent|all> when reviewing this topic.`
9462
+ });
9463
+ }
9464
+ if (sources.length >= 3) {
9465
+ findings.push({
9466
+ id: `consolidation_candidate:${topic}`,
9467
+ severity: "info",
9468
+ kind: "consolidation_candidate",
9469
+ topic,
9470
+ message: `topic ${topic} has ${sources.length} active memory sources and may be ready for consolidation`,
9471
+ suggested_action: "Review this topic in office-hours or proj-distill before promoting or merging related memory pages."
9472
+ });
9473
+ }
9474
+ }
9475
+ return findings.sort(compareMemoryReviewFindings);
9476
+ }
9477
+ function parseInvalidTopicWarning(warning) {
9478
+ const match = warning.match(/^(.*): invalid memory topic slug (.+)$/);
9479
+ if (!match) return null;
9480
+ return {
9481
+ path: match[1] ?? "",
9482
+ topic: match[2] ?? ""
9483
+ };
9484
+ }
9485
+ function groupSourcesByTopic(sources) {
9486
+ const byTopic = /* @__PURE__ */ new Map();
9487
+ for (const source of sources) {
9488
+ for (const topic of source.topics) {
9489
+ const list = byTopic.get(topic) ?? [];
9490
+ list.push(source);
9491
+ byTopic.set(topic, list);
9492
+ }
9493
+ }
9494
+ return byTopic;
9495
+ }
9496
+ function compareMemoryReviewFindings(a, b) {
9497
+ return severityRank(b.severity) - severityRank(a.severity) || a.kind.localeCompare(b.kind) || (a.topic ?? "").localeCompare(b.topic ?? "") || (a.path ?? "").localeCompare(b.path ?? "") || a.id.localeCompare(b.id);
9498
+ }
9499
+ function severityRank(severity) {
9500
+ return severity === "warn" ? 2 : 1;
9501
+ }
9293
9502
  function renderMemoryIndexStatusHint(status) {
9294
9503
  if (!status.cache_present) {
9295
9504
  return `memory index cache missing for ${status.project}; rebuild with memory index --project ${status.project}`;
@@ -9535,6 +9744,9 @@ function normalizeRecallScope(value) {
9535
9744
  if (!value) return void 0;
9536
9745
  return isRecallScope(value) ? value : void 0;
9537
9746
  }
9747
+ function isValidMemoryTopicSlug(value) {
9748
+ return /^[a-z0-9][a-z0-9-]*$/.test(value);
9749
+ }
9538
9750
  function isRecallScope(value) {
9539
9751
  return value === "project" || value === "global" || value === "cross-agent" || value === "all";
9540
9752
  }
@@ -9562,6 +9774,12 @@ function renderRecallHint(topic, sources) {
9562
9774
  if (sources.length === 0) return `no memory sources found for topic ${topic}`;
9563
9775
  return sources.map((source) => `${source.updated} ${source.title} (${source.path}) \u2014 ${source.summary}`).join("\n");
9564
9776
  }
9777
+ function renderMemoryReviewHint(project, status, findings) {
9778
+ const warns = findings.filter((finding) => finding.severity === "warn").length;
9779
+ const infos = findings.length - warns;
9780
+ const cacheStatus = !status.cache_present ? "cache missing" : status.stale ? "cache stale" : "cache current";
9781
+ return `memory review for ${project}: ${findings.length} finding(s) (${warns} warn, ${infos} info); ${cacheStatus}`;
9782
+ }
9565
9783
  function memoryCacheRelPath(project) {
9566
9784
  return `.skillwiki/memory/${project}/topics.json`;
9567
9785
  }
@@ -9582,46 +9800,11 @@ function dedupePages(pages) {
9582
9800
  }
9583
9801
  return out;
9584
9802
  }
9585
- async function readMemorySource(page, project, warnings) {
9586
- const text = await readPage(page);
9587
- const fm = extractFrontmatter(text);
9588
- if (!fm.ok) return null;
9589
- const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
9590
- if (topics.length === 0) return null;
9591
- const projects = memoryProjects(page.relPath, fm.data);
9592
- const scope = stringField2(fm.data.memory_scope);
9593
- if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
9594
- const privacy = stringField2(fm.data.memory_privacy) || "local";
9595
- if (privacy === "secret-blocked") {
9596
- warnings.push(`${page.relPath}: skipped secret-blocked memory`);
9597
- return null;
9598
- }
9599
- const status = stringField2(fm.data.memory_status) || "active";
9600
- if (status === "archived" || status === "rejected") return null;
9601
- const split = splitFrontmatter(text);
9602
- const body = split.ok ? split.data.body : text;
9603
- const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
9604
- const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
9605
- return {
9606
- path: page.relPath,
9607
- title,
9608
- summary: summarize2(body),
9609
- updated,
9610
- hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
9611
- topics,
9612
- project,
9613
- ...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
9614
- ...scope ? { memory_scope: scope } : {},
9615
- ...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
9616
- memory_privacy: privacy,
9617
- memory_status: status
9618
- };
9619
- }
9620
9803
  function normalizeMemoryTopics(value, path, warnings) {
9621
9804
  const rawTopics = typeof value === "string" ? [value] : stringArray(value);
9622
9805
  const topics = [];
9623
9806
  for (const topic of rawTopics) {
9624
- if (!/^[a-z0-9][a-z0-9-]*$/.test(topic)) {
9807
+ if (!isValidMemoryTopicSlug(topic)) {
9625
9808
  warnings.push(`${path}: invalid memory topic slug ${topic}`);
9626
9809
  continue;
9627
9810
  }
@@ -11836,6 +12019,15 @@ memoryCmd.command("recall [vault]").description("recall bounded source summaries
11836
12019
  limit: opts.limit
11837
12020
  }), v.vault, { postCommit: false });
11838
12021
  });
12022
+ memoryCmd.command("review [vault]").description("review deterministic memory gaps and cache drift without writing").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; writes are not supported on this surface", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
12023
+ const v = await resolveVaultArg(vault, opts.wiki);
12024
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
12025
+ else emit(await runMemoryReview({
12026
+ vault: v.vault,
12027
+ project: opts.project,
12028
+ dryRun: !!opts.dryRun
12029
+ }), v.vault, { postCommit: false });
12030
+ });
11839
12031
  memoryCmd.command("import [vault]").description("preview or apply local memory imports into raw captures").requiredOption("--from <path>", "source file or directory to scan").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; do not write captures", false).option("--apply", "write validated raw captures for ready entries", false).option("--max-bytes <n>", "reject source files above this size", (s) => parseInt(s, 10), 2e5).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
11840
12032
  const v = await resolveVaultArg(vault, opts.wiki);
11841
12033
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
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.9.22",
3
+ "version": "0.9.23",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",