skillwiki 0.9.22 → 0.9.25

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");
@@ -7003,9 +7004,12 @@ function summarizeChecks(checks) {
7003
7004
  function classifyLog(path, id, label, okPattern) {
7004
7005
  if (!existsSync12(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
7005
7006
  const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter(Boolean);
7006
- const last = lines[lines.length - 1] ?? "";
7007
- if (!last) return { id, label, status: "warn", detail: `log file empty: ${path}` };
7008
- if (/fail|error/i.test(last)) return { id, label, status: "error", detail: last.slice(0, 120) };
7007
+ if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
7008
+ const statusLine = [...lines].reverse().find(
7009
+ (line) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z /.test(line) && (okPattern.test(line) || /\bFAIL\b|\bERROR\b/i.test(line))
7010
+ );
7011
+ const last = statusLine ?? lines[lines.length - 1];
7012
+ if (/\bFAIL\b|\bERROR\b/i.test(last)) return { id, label, status: "error", detail: last.slice(0, 120) };
7009
7013
  if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
7010
7014
  return { id, label, status: "warn", detail: last.slice(0, 120) };
7011
7015
  }
@@ -9138,6 +9142,41 @@ async function runMemoryRecall(input) {
9138
9142
  })
9139
9143
  };
9140
9144
  }
9145
+ async function runMemoryReview(input) {
9146
+ const scan = await scanVault(input.vault);
9147
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
9148
+ const state = await buildMemoryIndexState(scan.data.allMarkdown, input.project);
9149
+ const checked = await checkMemoryIndex(input.vault, input.project, state);
9150
+ if (!checked.ok) {
9151
+ return {
9152
+ exitCode: checked.error.exitCode,
9153
+ result: checked.error.result
9154
+ };
9155
+ }
9156
+ const findings = buildMemoryReviewFindings({
9157
+ project: input.project,
9158
+ status: checked.data,
9159
+ indexState: state,
9160
+ reviewPages: state.reviewPages
9161
+ });
9162
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
9163
+ return {
9164
+ exitCode: ExitCode.OK,
9165
+ result: ok({
9166
+ project: input.project,
9167
+ generated_at: generatedAt,
9168
+ cache: {
9169
+ present: checked.data.cache_present,
9170
+ ...checked.data.generated_at ? { generated_at: checked.data.generated_at } : {},
9171
+ stale: checked.data.stale,
9172
+ source_hash_mismatches: checked.data.drift.changed_sources
9173
+ },
9174
+ findings,
9175
+ files_written: [],
9176
+ humanHint: renderMemoryReviewHint(input.project, checked.data, findings)
9177
+ })
9178
+ };
9179
+ }
9141
9180
  async function runMemoryImport(input) {
9142
9181
  const scan = await scanVault(input.vault);
9143
9182
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -9197,14 +9236,18 @@ async function buildMemoryIndexState(pages, project) {
9197
9236
  const warnings = [];
9198
9237
  const sourcePages = dedupePages(pages);
9199
9238
  const sources = [];
9239
+ const reviewPages = [];
9200
9240
  for (const page of sourcePages) {
9201
- const source = await readMemorySource(page, project, warnings);
9202
- if (source) sources.push(source);
9241
+ const memoryPage = await readMemoryPage(page, project, warnings);
9242
+ if (!memoryPage) continue;
9243
+ sources.push(memoryPage.source);
9244
+ if (memoryPage.reviewPage) reviewPages.push(memoryPage.reviewPage);
9203
9245
  }
9204
9246
  return {
9205
9247
  sources,
9206
9248
  topics: buildTopics(project, sources),
9207
- warnings
9249
+ warnings,
9250
+ reviewPages
9208
9251
  };
9209
9252
  }
9210
9253
  async function checkMemoryIndex(vault, project, current) {
@@ -9290,6 +9333,175 @@ function emptyMemoryIndexDrift() {
9290
9333
  changed_sources: []
9291
9334
  };
9292
9335
  }
9336
+ async function readMemoryPage(page, project, warnings) {
9337
+ const text = await readPage(page);
9338
+ const fm = extractFrontmatter(text);
9339
+ if (!fm.ok) return null;
9340
+ const rawTopics = typeof fm.data.memory_topics === "string" ? [fm.data.memory_topics] : stringArray(fm.data.memory_topics);
9341
+ if (rawTopics.length === 0) return null;
9342
+ const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
9343
+ if (topics.length === 0) return null;
9344
+ const scope = stringField2(fm.data.memory_scope);
9345
+ const projects = memoryProjects(page.relPath, fm.data);
9346
+ if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
9347
+ const privacy = stringField2(fm.data.memory_privacy) || "local";
9348
+ if (privacy === "secret-blocked") {
9349
+ warnings.push(`${page.relPath}: skipped secret-blocked memory`);
9350
+ return null;
9351
+ }
9352
+ const status = stringField2(fm.data.memory_status) || "active";
9353
+ if (status === "archived" || status === "rejected") return null;
9354
+ const metadataGaps = [];
9355
+ if (!scope) metadataGaps.push("memory_scope");
9356
+ if (!stringField2(fm.data.memory_policy)) metadataGaps.push("memory_policy");
9357
+ if (!stringField2(fm.data.memory_privacy)) metadataGaps.push("memory_privacy");
9358
+ if (!stringField2(fm.data.memory_status)) metadataGaps.push("memory_status");
9359
+ if (!stringField2(fm.data.last_seen) && !stringField2(fm.data.updated) && !stringField2(fm.data.ingested)) {
9360
+ metadataGaps.push("freshness");
9361
+ }
9362
+ const split = splitFrontmatter(text);
9363
+ const body = split.ok ? split.data.body : text;
9364
+ const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
9365
+ const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
9366
+ return {
9367
+ source: {
9368
+ path: page.relPath,
9369
+ title,
9370
+ summary: summarize2(body),
9371
+ updated,
9372
+ hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
9373
+ topics,
9374
+ project,
9375
+ ...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
9376
+ ...scope ? { memory_scope: scope } : {},
9377
+ ...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
9378
+ memory_privacy: privacy,
9379
+ memory_status: status
9380
+ },
9381
+ ...metadataGaps.length > 0 ? {
9382
+ reviewPage: {
9383
+ path: page.relPath,
9384
+ metadataGaps
9385
+ }
9386
+ } : {}
9387
+ };
9388
+ }
9389
+ function buildMemoryReviewFindings(input) {
9390
+ const findings = [];
9391
+ if (!input.status.cache_present) {
9392
+ findings.push({
9393
+ id: `cache_missing:${input.project}`,
9394
+ severity: "warn",
9395
+ kind: "cache_missing",
9396
+ message: `memory cache for ${input.project} is missing`,
9397
+ suggested_action: `Run skillwiki memory index --project ${input.project} to build the local cache.`
9398
+ });
9399
+ } else if (input.status.stale) {
9400
+ findings.push({
9401
+ id: `cache_stale:${input.project}`,
9402
+ severity: "warn",
9403
+ kind: "cache_stale",
9404
+ 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)`,
9405
+ suggested_action: `Run skillwiki memory index --project ${input.project} or --if-stale to refresh the local cache.`
9406
+ });
9407
+ }
9408
+ for (const path of input.status.drift.changed_sources) {
9409
+ findings.push({
9410
+ id: `source_hash_drift:${path}`,
9411
+ severity: "warn",
9412
+ kind: "source_hash_drift",
9413
+ path,
9414
+ message: `${path} changed since the last cache build`,
9415
+ suggested_action: `Refresh the cache for ${input.project} and review whether the source summary still matches.`
9416
+ });
9417
+ }
9418
+ for (const warning of input.indexState.warnings) {
9419
+ const parsed = parseInvalidTopicWarning(warning);
9420
+ if (!parsed) continue;
9421
+ findings.push({
9422
+ id: `invalid_topic_slug:${parsed.path}:${parsed.topic}`,
9423
+ severity: "warn",
9424
+ kind: "invalid_topic_slug",
9425
+ path: parsed.path,
9426
+ topic: parsed.topic,
9427
+ message: `${parsed.path} declares invalid memory topic slug ${parsed.topic}`,
9428
+ suggested_action: "Normalize the topic slug to lowercase kebab-case so memory index and review can include it."
9429
+ });
9430
+ }
9431
+ for (const page of input.reviewPages) {
9432
+ if (page.metadataGaps.length === 0) continue;
9433
+ findings.push({
9434
+ id: `metadata_gap:${page.path}`,
9435
+ severity: "warn",
9436
+ kind: "metadata_gap",
9437
+ path: page.path,
9438
+ message: `${page.path} is missing explicit memory metadata: ${page.metadataGaps.join(", ")}`,
9439
+ suggested_action: "Add the missing frontmatter fields before relying on this source for cross-device memory review."
9440
+ });
9441
+ }
9442
+ const byTopic = groupSourcesByTopic(input.indexState.sources);
9443
+ for (const [topic, sources] of byTopic.entries()) {
9444
+ if (sources.length === 1) {
9445
+ findings.push({
9446
+ id: `sparse_topic:${topic}`,
9447
+ severity: "info",
9448
+ kind: "sparse_topic",
9449
+ topic,
9450
+ ...sources[0]?.path ? { path: sources[0].path } : {},
9451
+ message: `topic ${topic} has only one active memory source`,
9452
+ suggested_action: "Decide whether the topic needs more coverage or should stay intentionally narrow."
9453
+ });
9454
+ }
9455
+ const hasProjectSource = sources.some((source) => isProjectMemorySource(source, input.project));
9456
+ const hasNonProjectScope = sources.some((source) => source.memory_scope === "global" || source.memory_scope === "cross-agent");
9457
+ if (hasProjectSource && hasNonProjectScope) {
9458
+ findings.push({
9459
+ id: `scope_gap:${topic}`,
9460
+ severity: "info",
9461
+ kind: "scope_gap",
9462
+ topic,
9463
+ message: `topic ${topic} mixes project and non-project memory scopes`,
9464
+ suggested_action: `Use skillwiki memory recall --project ${input.project} --topic ${topic} --scope <project|global|cross-agent|all> when reviewing this topic.`
9465
+ });
9466
+ }
9467
+ if (sources.length >= 3) {
9468
+ findings.push({
9469
+ id: `consolidation_candidate:${topic}`,
9470
+ severity: "info",
9471
+ kind: "consolidation_candidate",
9472
+ topic,
9473
+ message: `topic ${topic} has ${sources.length} active memory sources and may be ready for consolidation`,
9474
+ suggested_action: "Review this topic in office-hours or proj-distill before promoting or merging related memory pages."
9475
+ });
9476
+ }
9477
+ }
9478
+ return findings.sort(compareMemoryReviewFindings);
9479
+ }
9480
+ function parseInvalidTopicWarning(warning) {
9481
+ const match = warning.match(/^(.*): invalid memory topic slug (.+)$/);
9482
+ if (!match) return null;
9483
+ return {
9484
+ path: match[1] ?? "",
9485
+ topic: match[2] ?? ""
9486
+ };
9487
+ }
9488
+ function groupSourcesByTopic(sources) {
9489
+ const byTopic = /* @__PURE__ */ new Map();
9490
+ for (const source of sources) {
9491
+ for (const topic of source.topics) {
9492
+ const list = byTopic.get(topic) ?? [];
9493
+ list.push(source);
9494
+ byTopic.set(topic, list);
9495
+ }
9496
+ }
9497
+ return byTopic;
9498
+ }
9499
+ function compareMemoryReviewFindings(a, b) {
9500
+ 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);
9501
+ }
9502
+ function severityRank(severity) {
9503
+ return severity === "warn" ? 2 : 1;
9504
+ }
9293
9505
  function renderMemoryIndexStatusHint(status) {
9294
9506
  if (!status.cache_present) {
9295
9507
  return `memory index cache missing for ${status.project}; rebuild with memory index --project ${status.project}`;
@@ -9535,6 +9747,9 @@ function normalizeRecallScope(value) {
9535
9747
  if (!value) return void 0;
9536
9748
  return isRecallScope(value) ? value : void 0;
9537
9749
  }
9750
+ function isValidMemoryTopicSlug(value) {
9751
+ return /^[a-z0-9][a-z0-9-]*$/.test(value);
9752
+ }
9538
9753
  function isRecallScope(value) {
9539
9754
  return value === "project" || value === "global" || value === "cross-agent" || value === "all";
9540
9755
  }
@@ -9562,6 +9777,12 @@ function renderRecallHint(topic, sources) {
9562
9777
  if (sources.length === 0) return `no memory sources found for topic ${topic}`;
9563
9778
  return sources.map((source) => `${source.updated} ${source.title} (${source.path}) \u2014 ${source.summary}`).join("\n");
9564
9779
  }
9780
+ function renderMemoryReviewHint(project, status, findings) {
9781
+ const warns = findings.filter((finding) => finding.severity === "warn").length;
9782
+ const infos = findings.length - warns;
9783
+ const cacheStatus = !status.cache_present ? "cache missing" : status.stale ? "cache stale" : "cache current";
9784
+ return `memory review for ${project}: ${findings.length} finding(s) (${warns} warn, ${infos} info); ${cacheStatus}`;
9785
+ }
9565
9786
  function memoryCacheRelPath(project) {
9566
9787
  return `.skillwiki/memory/${project}/topics.json`;
9567
9788
  }
@@ -9582,46 +9803,11 @@ function dedupePages(pages) {
9582
9803
  }
9583
9804
  return out;
9584
9805
  }
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
9806
  function normalizeMemoryTopics(value, path, warnings) {
9621
9807
  const rawTopics = typeof value === "string" ? [value] : stringArray(value);
9622
9808
  const topics = [];
9623
9809
  for (const topic of rawTopics) {
9624
- if (!/^[a-z0-9][a-z0-9-]*$/.test(topic)) {
9810
+ if (!isValidMemoryTopicSlug(topic)) {
9625
9811
  warnings.push(`${path}: invalid memory topic slug ${topic}`);
9626
9812
  continue;
9627
9813
  }
@@ -11836,6 +12022,15 @@ memoryCmd.command("recall [vault]").description("recall bounded source summaries
11836
12022
  limit: opts.limit
11837
12023
  }), v.vault, { postCommit: false });
11838
12024
  });
12025
+ 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) => {
12026
+ const v = await resolveVaultArg(vault, opts.wiki);
12027
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
12028
+ else emit(await runMemoryReview({
12029
+ vault: v.vault,
12030
+ project: opts.project,
12031
+ dryRun: !!opts.dryRun
12032
+ }), v.vault, { postCommit: false });
12033
+ });
11839
12034
  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
12035
  const v = await resolveVaultArg(vault, opts.wiki);
11841
12036
  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.25",
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.25",
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.25",
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.25",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",