@zosmaai/pi-llm-wiki 0.10.9 → 0.11.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.de.md +35 -4
  3. package/README.es.md +260 -170
  4. package/README.fr.md +35 -4
  5. package/README.hi.md +35 -4
  6. package/README.ja.md +35 -4
  7. package/README.ko.md +35 -4
  8. package/README.md +31 -10
  9. package/README.pt.md +35 -4
  10. package/README.ru.md +35 -4
  11. package/README.zh.md +260 -170
  12. package/assets/demo.gif +0 -0
  13. package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
  14. package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
  15. package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
  16. package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
  17. package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
  18. package/dist/extensions/llm-wiki/lib/inject.js +65 -0
  19. package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
  20. package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
  21. package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
  22. package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
  23. package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
  24. package/dist/extensions/llm-wiki/lib/observation.js +283 -0
  25. package/dist/extensions/llm-wiki/lib/recall.js +875 -0
  26. package/dist/extensions/llm-wiki/lib/retro.js +158 -0
  27. package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
  28. package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
  29. package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
  30. package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
  31. package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
  32. package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
  33. package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
  34. package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
  35. package/dist/extensions/llm-wiki/lib/utils.js +347 -0
  36. package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
  37. package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
  38. package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
  39. package/dist/mcp/exec.js +121 -0
  40. package/dist/mcp/index.js +229 -0
  41. package/dist/mcp/operations.js +130 -0
  42. package/dist/package.json +1 -0
  43. package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
  44. package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
  45. package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
  46. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
  47. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
  48. package/extensions/llm-wiki/index.ts +22 -36
  49. package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
  50. package/extensions/llm-wiki/lib/embeddings.ts +9 -3
  51. package/extensions/llm-wiki/lib/guardrails.ts +26 -18
  52. package/extensions/llm-wiki/lib/indexing.ts +2 -1
  53. package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
  54. package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
  55. package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
  56. package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
  57. package/extensions/llm-wiki/lib/metadata.ts +531 -116
  58. package/extensions/llm-wiki/lib/observation.ts +37 -43
  59. package/extensions/llm-wiki/lib/recall.ts +61 -33
  60. package/extensions/llm-wiki/lib/retro.ts +65 -41
  61. package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
  62. package/extensions/llm-wiki/lib/source-packet.ts +44 -31
  63. package/extensions/llm-wiki/lib/tools.ts +404 -342
  64. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  65. package/extensions/llm-wiki/lib/utils.ts +120 -131
  66. package/extensions/llm-wiki/lib/vault-format.ts +363 -0
  67. package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
  68. package/mcp/exec.ts +122 -0
  69. package/mcp/index.ts +60 -250
  70. package/mcp/operations.ts +176 -0
  71. package/package.json +8 -2
  72. package/scripts/migrate-llm-wiki.js +801 -0
  73. package/skills/llm-wiki/SKILL.md +8 -6
@@ -2,26 +2,24 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
2
2
  import { join, relative } from "node:path";
3
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
+ import { bootstrapVault } from "./bootstrap.js";
5
6
  import { launchEmbedPages, reindexEmbeddings, resolveEmbedder } from "./embeddings.js";
6
7
  import { scheduleReindex } from "./indexing.js";
7
8
  import { runIngestSynthesis } from "./ingest-worker.js";
8
9
  import {
9
- type Registry,
10
- appendEvent,
11
- buildBacklinks,
12
- buildRegistry,
13
- rebuildMetadata,
14
- rebuildMetadataLight,
15
- } from "./metadata.js";
10
+ createKnowledgeDocument,
11
+ serializeKnowledgeDocument,
12
+ writeKnowledgeDocumentFile,
13
+ } from "./knowledge-document.js";
14
+ import { buildResolvedBacklinks } from "./knowledge-links.js";
15
+ import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
16
+ import { type Registry, appendEvent, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
16
17
  import type { Runtime } from "./runtime.js";
17
18
  import { captureFile, captureText, captureUrl } from "./source-packet.js";
18
19
  import { parseModelRef } from "./task-config.js";
19
20
  import {
20
21
  type VaultPaths,
21
22
  detectVaultFormat,
22
- ensureVaultStructure,
23
- extractWikilinks,
24
- findWikiPages,
25
23
  fmtDate,
26
24
  getVaultPaths,
27
25
  readJson,
@@ -29,6 +27,14 @@ import {
29
27
  slugify,
30
28
  writeJson,
31
29
  } from "./utils.js";
30
+ import {
31
+ assertWritableVault,
32
+ compareCodePoint,
33
+ discoverKnowledgeDocuments,
34
+ inspectVaultFormat,
35
+ inspectWritableVault,
36
+ } from "./vault-format.js";
37
+ import { getWikiStatus, searchRegistry } from "./wiki-service.js";
32
38
 
33
39
  /**
34
40
  * All LLM Wiki custom tools.
@@ -116,59 +122,35 @@ export function registerWikiBootstrap(pi: ExtensionAPI): void {
116
122
  const root = params.root ?? ctx.cwd ?? process.cwd();
117
123
  const mode = params.mode || "personal";
118
124
  const paths = getVaultPaths(root);
119
-
120
- ensureVaultStructure(paths);
121
-
122
- const config = {
123
- name: params.topic,
124
- mode,
125
- topic: params.topic,
126
- created: fmtDate(),
127
- version: "1.0",
128
- };
129
- writeJson(join(paths.dotWiki, "config.json"), config);
130
-
131
- const schema = [
132
- "# LLM Wiki Schema",
133
- "",
134
- "## Ownership Rules",
135
- "",
136
- "| Path | Owner | Rule |",
137
- "|------|-------|------|",
138
- "| raw/** | extension | immutable after capture |",
139
- "| wiki/** | model + user | editable knowledge pages |",
140
- "| meta/* | extension | auto-generated |",
141
- "| . | human + explicit request | operating rules |",
142
- "",
143
- "## Source Packet Format",
144
- "",
145
- "```",
146
- "raw/sources/SRC-YYYY-MM-DD-NNN/",
147
- " manifest.json",
148
- " original/",
149
- " extracted.md",
150
- " attachments/",
151
- "```",
152
- "",
153
- "## Page Types",
154
- "",
155
- "- **source** — what this specific source says",
156
- "- **entity** — people, orgs, tools, products",
157
- "- **concept** — ideas, patterns, frameworks",
158
- "- **synthesis** — cross-source theses and tensions",
159
- "- **analysis** — durable filed answers from queries",
160
- "- **requirement** — atomic requirements with status, priority, and traceability",
161
- "",
162
- "## Linking Style",
163
- "",
164
- "- Internal: [[folder/page-name]]",
165
- "- Citation: [[sources/SRC-YYYY-MM-DD-NNN]]",
166
- "",
167
- ].join("\n");
168
- writeFileSync(join(paths.dotWiki, "WIKI_SCHEMA.md"), schema, "utf-8");
169
-
170
- rebuildMetadata(paths);
171
- appendEvent(paths, { kind: "bootstrap", topic: params.topic, mode });
125
+ const result = bootstrapVault(paths, { topic: params.topic, mode });
126
+ if (!result.ok) {
127
+ return {
128
+ content: [{ type: "text", text: `Wiki vault error: ${result.diagnostics[0].message}` }],
129
+ details: {
130
+ error: result.diagnostics[0].code,
131
+ diagnostics: result.diagnostics,
132
+ } as Record<string, unknown>,
133
+ isError: true,
134
+ };
135
+ }
136
+ if (!result.projection.ok) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: `✅ Wiki bootstrapped but projection rebuild had issues: ${result.projection.diagnostics
142
+ .map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
143
+ .join("; ")}`,
144
+ },
145
+ ],
146
+ details: {
147
+ root,
148
+ mode,
149
+ topic: params.topic,
150
+ diagnostics: result.projection.diagnostics,
151
+ } as Record<string, unknown>,
152
+ };
153
+ }
172
154
 
173
155
  return {
174
156
  content: [
@@ -216,11 +198,16 @@ export function registerWikiCaptureSource(pi: ExtensionAPI, runtime?: Runtime):
216
198
  }),
217
199
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
218
200
  const paths = getPaths(ctx.cwd);
219
- const vaultCheck = requireVault(paths);
201
+ const vaultCheck = inspectWritableVault(paths);
220
202
  if (!vaultCheck.ok) {
221
203
  return {
222
- content: [{ type: "text", text: vaultCheck.reason }],
223
- details: { error: vaultCheck.reason } as Record<string, unknown>,
204
+ content: [
205
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
206
+ ],
207
+ details: {
208
+ error: vaultCheck.diagnostics[0].code,
209
+ diagnostics: vaultCheck.diagnostics,
210
+ } as Record<string, unknown>,
224
211
  isError: true,
225
212
  };
226
213
  }
@@ -315,11 +302,16 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
315
302
  }),
316
303
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
317
304
  const paths = getPaths(ctx.cwd);
318
- const vaultCheck = requireVault(paths);
305
+ const vaultCheck = inspectWritableVault(paths);
319
306
  if (!vaultCheck.ok) {
320
307
  return {
321
- content: [{ type: "text", text: vaultCheck.reason }],
322
- details: { error: vaultCheck.reason } as Record<string, unknown>,
308
+ content: [
309
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
310
+ ],
311
+ details: {
312
+ error: vaultCheck.diagnostics[0].code,
313
+ diagnostics: vaultCheck.diagnostics,
314
+ } as Record<string, unknown>,
323
315
  isError: true,
324
316
  };
325
317
  }
@@ -528,11 +520,16 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
528
520
  }),
529
521
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
530
522
  const paths = getPaths(ctx.cwd);
531
- const vaultCheck = requireVault(paths);
523
+ const vaultCheck = inspectWritableVault(paths);
532
524
  if (!vaultCheck.ok) {
533
525
  return {
534
- content: [{ type: "text", text: vaultCheck.reason }],
535
- details: { error: vaultCheck.reason } as Record<string, unknown>,
526
+ content: [
527
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
528
+ ],
529
+ details: {
530
+ error: vaultCheck.diagnostics[0].code,
531
+ diagnostics: vaultCheck.diagnostics,
532
+ } as Record<string, unknown>,
536
533
  isError: true,
537
534
  };
538
535
  }
@@ -567,9 +564,19 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
567
564
  }
568
565
 
569
566
  const today = fmtDate();
570
- const template = buildPageTemplate(type, params.title, today, params.content);
567
+ const body = params.content ?? buildPageBody(type, params.title);
568
+ const doc = createKnowledgeDocument(
569
+ `${folder}/${slug}.md`,
570
+ {
571
+ type,
572
+ title: params.title,
573
+ created: today,
574
+ updated: today,
575
+ },
576
+ body,
577
+ );
571
578
  mkdirSync(join(paths.wiki, folder), { recursive: true });
572
- writeFileSync(pagePath, template, "utf-8");
579
+ writeKnowledgeDocumentFile(pagePath, doc);
573
580
 
574
581
  appendEvent(paths, {
575
582
  kind: "ensure_page",
@@ -595,151 +602,135 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
595
602
  });
596
603
  }
597
604
 
598
- function buildPageTemplate(
599
- type: string,
600
- title: string,
601
- date: string,
602
- customContent?: string,
603
- ): string {
604
- if (customContent) return customContent;
605
+ function buildPageBody(type: string, title: string): string {
606
+ if (type === "entity") {
607
+ return `# ${title}
608
+
609
+ One-line description.
605
610
 
606
- const base = `---\ntype: ${type}\ncreated: ${date}\nupdated: ${date}\nsources: []\n---\n\n# ${title}\n\n[Description to be filled]\n\n## Links\n\n- [[related-page]]\n`;
611
+ ## Overview
607
612
 
608
- if (type === "entity") {
609
- return base
610
- .replace("[Description to be filled]", "One-line description.\n\n## Overview\n\n[Key facts]")
611
- .replace("type: entity", "type: entity\ncategory: organization");
613
+ [Key facts]
614
+
615
+ ## Links
616
+
617
+ - [related-page](/concepts/related-page.md)`;
612
618
  }
613
619
  if (type === "concept") {
614
- return base
615
- .replace(
616
- "[Description to be filled]",
617
- "One-line definition.\n\n## Definition\n\n[Clear explanation]",
618
- )
619
- .replace("type: concept", "type: concept\ndomain: ai");
620
+ return `# ${title}
621
+
622
+ One-line definition.
623
+
624
+ ## Definition
625
+
626
+ [Clear explanation]
627
+
628
+ ## Links
629
+
630
+ - [related-page](/concepts/related-page.md)`;
620
631
  }
621
632
  if (type === "synthesis") {
622
- return base
623
- .replace(
624
- "[Description to be filled]",
625
- "Cross-cutting analysis.\n\n## Question\n\n[What drove this?]",
626
- )
627
- .replace("sources: []", "sources_count: 0");
633
+ return `# ${title}
634
+
635
+ Cross-cutting analysis.
636
+
637
+ ## Question
638
+
639
+ [What drove this?]
640
+
641
+ ## Links
642
+
643
+ - [related-page](/concepts/related-page.md)`;
628
644
  }
629
645
  if (type === "analysis") {
630
- return base.replace(
631
- "[Description to be filled]",
632
- "Durable answer from a query.\n\n## Question\n\n[Original question]",
633
- );
646
+ return `# ${title}
647
+
648
+ Durable answer from a query.
649
+
650
+ ## Question
651
+
652
+ [Original question]
653
+
654
+ ## Links
655
+
656
+ - [related-page](/concepts/related-page.md)`;
634
657
  }
635
658
  if (type === "skill") {
636
- return [
637
- "---",
638
- "type: skill",
639
- `created: ${date}`,
640
- `updated: ${date}`,
641
- "status: draft",
642
- "trajectories: []",
643
- "tags: []",
644
- "---",
645
- "",
646
- `# ${title}`,
647
- "",
648
- "_One-line summary of the reusable pattern this skill captures._",
649
- "",
650
- "## When to Use",
651
- "",
652
- "[Trigger conditions — when this pattern applies]",
653
- "",
654
- "## Procedure",
655
- "",
656
- "1. [Step 1]",
657
- "2. [Step 2]",
658
- "",
659
- "## Pitfalls",
660
- "",
661
- "- [Known failure mode or caveat]",
662
- "",
663
- "## Distilled From",
664
- "",
665
- "_Trajectories this skill was generalized from._",
666
- "",
667
- "- [[trajectories/TRJ-...]]",
668
- "",
669
- ].join("\n");
659
+ return `# ${title}
660
+
661
+ _One-line summary of the reusable pattern this skill captures._
662
+
663
+ ## When to Use
664
+
665
+ [Trigger conditions — when this pattern applies]
666
+
667
+ ## Procedure
668
+
669
+ 1. [Step 1]
670
+ 2. [Step 2]
671
+
672
+ ## Pitfalls
673
+
674
+ - [Known failure mode or caveat]
675
+
676
+ ## Distilled From
677
+
678
+ _Trajectories this skill was generalized from._
679
+
680
+ - [trajectories/TRJ-...](/trajectories/TRJ-....md)`;
670
681
  }
671
682
  if (type === "case") {
672
- return [
673
- "---",
674
- "type: case",
675
- `created: ${date}`,
676
- `updated: ${date}`,
677
- "status: draft",
678
- "outcome: success",
679
- "trajectory_id: ",
680
- "tags: []",
681
- "---",
682
- "",
683
- `# ${title}`,
684
- "",
685
- "_One-line summary of the specific task this case records._",
686
- "",
687
- "## Task",
688
- "",
689
- "[What was requested]",
690
- "",
691
- "## Approach",
692
- "",
693
- "[How the agent solved it — key steps and decisions]",
694
- "",
695
- "## Outcome",
696
- "",
697
- "[Result, and anything worth reusing or avoiding next time]",
698
- "",
699
- "## Trajectory",
700
- "",
701
- "- [[trajectories/TRJ-...]] — captured tool-call run",
702
- "",
703
- ].join("\n");
683
+ return `# ${title}
684
+
685
+ _One-line summary of the specific task this case records._
686
+
687
+ ## Task
688
+
689
+ [What was requested]
690
+
691
+ ## Approach
692
+
693
+ [How the agent solved it — key steps and decisions]
694
+
695
+ ## Outcome
696
+
697
+ [Result, and anything worth reusing or avoiding next time]
698
+
699
+ ## Trajectory
700
+
701
+ - [trajectories/TRJ-...](/trajectories/TRJ-....md) — captured tool-call run`;
704
702
  }
705
703
  if (type === "requirement") {
706
- return [
707
- "---",
708
- "type: requirement",
709
- `created: ${date}`,
710
- `updated: ${date}`,
711
- "status: draft",
712
- "priority: p2",
713
- "source_id: ",
714
- "depends_on: []",
715
- "---",
716
- "",
717
- `# ${title}`,
718
- "",
719
- "## Description",
720
- "",
721
- "[Clear description of what this requirement entails]",
722
- "",
723
- "## Acceptance Criteria",
724
- "",
725
- "- [ ] [Criterion 1]",
726
- "- [ ] [Criterion 2]",
727
- "",
728
- "## Dependencies",
729
- "",
730
- "_Pages this requirement depends on._",
731
- "",
732
- "## Implementation Notes",
733
- "",
734
- "[Optional notes]",
735
- "",
736
- "## Sources",
737
- "",
738
- "- [[sources/SRC-...]] — original concept capture",
739
- "",
740
- ].join("\n");
704
+ return `# ${title}
705
+
706
+ ## Description
707
+
708
+ [Clear description of what this requirement entails]
709
+
710
+ ## Acceptance Criteria
711
+
712
+ - [ ] [Criterion 1]
713
+ - [ ] [Criterion 2]
714
+
715
+ ## Dependencies
716
+
717
+ _Pages this requirement depends on._
718
+
719
+ ## Implementation Notes
720
+
721
+ [Optional notes]
722
+
723
+ ## Sources
724
+
725
+ - [sources/SRC-...](/sources/SRC-....md) — original concept capture`;
741
726
  }
742
- return base;
727
+ return `# ${title}
728
+
729
+ [Description to be filled]
730
+
731
+ ## Links
732
+
733
+ - [related-page](/concepts/related-page.md)`;
743
734
  }
744
735
 
745
736
  // ─── 5. wiki_search ─────────────────────────────────────
@@ -757,29 +748,15 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
757
748
  }),
758
749
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
759
750
  const paths = getPaths(ctx.cwd);
760
- const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
761
- version: "1.0",
762
- last_updated: "",
763
- pages: {},
764
- });
765
- const q = params.query.toLowerCase();
766
-
767
- const matches = Object.entries(registry.pages)
768
- .filter(([id, entry]) => {
769
- const matchesQuery =
770
- id.toLowerCase().includes(q) ||
771
- String(entry.title).toLowerCase().includes(q) ||
772
- String(entry.type).toLowerCase().includes(q);
773
- const matchesType =
774
- !params.type || String(entry.type).toLowerCase() === params.type.toLowerCase();
775
- return matchesQuery && matchesType;
776
- })
777
- .map(([id, entry]) => ({ id, title: entry.title, type: entry.type }));
778
-
779
- if (matches.length === 0) {
751
+ const result = searchRegistry(paths, params.query, params.type);
752
+
753
+ if (result.matches.length === 0) {
780
754
  return {
781
755
  content: [{ type: "text", text: `No pages found for "${params.query}"` }],
782
- details: { query: params.query, matches: [] } as Record<string, unknown>,
756
+ details: { query: params.query, matches: [], diagnostics: result.diagnostics } as Record<
757
+ string,
758
+ unknown
759
+ >,
783
760
  };
784
761
  }
785
762
 
@@ -788,13 +765,17 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
788
765
  {
789
766
  type: "text",
790
767
  text: [
791
- `🔍 **${matches.length} result(s)** for "${params.query}":`,
768
+ `🔍 **${result.matches.length} result(s)** for "${params.query}":`,
792
769
  "",
793
- ...matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
770
+ ...result.matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
794
771
  ].join("\n"),
795
772
  },
796
773
  ],
797
- details: { query: params.query, matches } as Record<string, unknown>,
774
+ details: {
775
+ query: params.query,
776
+ matches: result.matches,
777
+ diagnostics: result.diagnostics,
778
+ } as Record<string, unknown>,
798
779
  };
799
780
  },
800
781
  });
@@ -820,11 +801,16 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
820
801
  }),
821
802
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
822
803
  const paths = getPaths(ctx.cwd);
823
- const vaultCheck = requireVault(paths);
804
+ const vaultCheck = inspectWritableVault(paths);
824
805
  if (!vaultCheck.ok) {
825
806
  return {
826
- content: [{ type: "text", text: vaultCheck.reason }],
827
- details: { error: vaultCheck.reason } as Record<string, unknown>,
807
+ content: [
808
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
809
+ ],
810
+ details: {
811
+ error: vaultCheck.diagnostics[0].code,
812
+ diagnostics: vaultCheck.diagnostics,
813
+ } as Record<string, unknown>,
828
814
  isError: true,
829
815
  };
830
816
  }
@@ -846,82 +832,117 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
846
832
  * run off-thread via `dispatchReported`). Returns the human-readable summary.
847
833
  */
848
834
  function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
849
- const pages = findWikiPages(paths.wiki);
850
- const registry = buildRegistry(paths);
851
- buildBacklinks(paths, registry); // ensures backlinks.json is current
835
+ assertWritableVault(paths);
836
+ let repair: ReturnType<typeof repairLegacyKnowledgeDocuments> | undefined;
837
+ if (autoFix) {
838
+ let projection = rebuildMetadata(paths);
839
+ repair = !projection.ok ? repairLegacyKnowledgeDocuments(paths) : undefined;
840
+ if (repair?.repaired) projection = rebuildMetadata(paths);
841
+ if (!projection.ok) {
842
+ return [
843
+ "# Wiki Lint Report",
844
+ "",
845
+ repair?.repaired ? `Legacy pages repaired: ${repair.repaired}` : "",
846
+ repair?.manifestPath ? `Repair manifest: ${repair.manifestPath}` : "",
847
+ "Projection-blocking diagnostics:",
848
+ ...projection.diagnostics.map(
849
+ (diagnostic) => `- ${diagnostic.code}: ${diagnostic.path}: ${diagnostic.message}`,
850
+ ),
851
+ ]
852
+ .filter(Boolean)
853
+ .join("\n");
854
+ }
855
+ } else {
856
+ const vault = inspectVaultFormat(paths);
857
+ const audit = discoverKnowledgeDocuments(paths);
858
+ const diagnostics = [...vault.diagnostics, ...audit.diagnostics];
859
+ if (vault.blocking || audit.blocking) {
860
+ return [
861
+ "# Wiki Lint Report",
862
+ "",
863
+ "Projection-blocking diagnostics:",
864
+ ...diagnostics.map(
865
+ (diagnostic) => `- ${diagnostic.code}: ${diagnostic.path}: ${diagnostic.message}`,
866
+ ),
867
+ ].join("\n");
868
+ }
869
+ }
852
870
 
871
+ const discovery = discoverKnowledgeDocuments(paths);
872
+ const pages = discovery.documents;
873
+ const knownIds = new Set(pages.map((page) => page.id));
874
+ const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
875
+ const gapSources = new Map<string, Set<string>>();
853
876
  const findings: string[] = [];
854
- let orphans = 0;
855
877
  let missingPages = 0;
856
878
  let contradictions = 0;
857
- const gaps: Array<{ topic: string; mentionedBy: string[] }> = [];
858
-
859
- const allPageIds = new Set(pages.map((p) => p.relative));
860
- const linkCounts: Record<string, number> = {};
861
879
 
862
880
  for (const page of pages) {
863
- const links = extractWikilinks(page.content);
864
- for (const link of links) {
865
- if (!allPageIds.has(link)) {
866
- missingPages++;
867
- findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
868
- const existing = gaps.find((g) => g.topic === link);
869
- if (existing) {
870
- if (!existing.mentionedBy.includes(page.relative))
871
- existing.mentionedBy.push(page.relative);
872
- } else {
873
- gaps.push({ topic: link, mentionedBy: [page.relative] });
874
- }
875
- } else {
876
- linkCounts[link] = (linkCounts[link] || 0) + 1;
877
- }
881
+ const resolved = buildResolvedBacklinks(page.id, page.body, knownIds);
882
+ for (const target of resolved.targets) inbound[target]++;
883
+ for (const unresolved of resolved.unresolved) {
884
+ const sources = gapSources.get(unresolved.target) ?? new Set<string>();
885
+ sources.add(page.id);
886
+ gapSources.set(unresolved.target, sources);
887
+ missingPages++;
888
+ findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
878
889
  }
879
890
  }
880
891
 
892
+ let orphans = 0;
881
893
  for (const page of pages) {
882
- if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
894
+ if (inbound[page.id] === 0) {
883
895
  orphans++;
884
- findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
896
+ findings.push(`Orphan: ${page.id} has no inbound links`);
885
897
  }
886
- }
887
-
888
- for (const page of pages) {
889
- if (page.content.includes("⚠️ **Contradiction")) {
898
+ if (page.body.includes("⚠️ **Contradiction")) {
890
899
  contradictions++;
891
- findings.push(`Contradiction flagged in [[${page.relative}]]`);
900
+ findings.push(`Contradiction flagged in ${page.id}`);
892
901
  }
893
902
  }
894
903
 
904
+ const gaps = [...gapSources.entries()]
905
+ .map(([topic, sources]) => ({ topic, mentionedBy: [...sources].sort(compareCodePoint) }))
906
+ .sort((left, right) => compareCodePoint(left.topic, right.topic));
895
907
  let fixesApplied = 0;
896
908
  if (autoFix) {
897
909
  for (const gap of gaps) {
898
- if (gap.mentionedBy.length >= 2) {
899
- const folder = gap.topic.includes("/") ? gap.topic.split("/")[0] : "concepts";
900
- const name = gap.topic.includes("/") ? gap.topic.split("/").pop()! : gap.topic;
901
- const pagePath = join(paths.wiki, folder, `${name}.md`);
902
- mkdirSync(join(paths.wiki, folder), { recursive: true });
903
- try {
904
- // Atomic create-if-absent: the `wx` flag fails with EEXIST instead of
905
- // overwriting, avoiding the existsSync→write TOCTOU race (CodeQL).
906
- writeFileSync(
907
- pagePath,
908
- `---\ntype: concept\ncreated: ${fmtDate()}\nupdated: ${fmtDate()}\nsources: []\nstatus: stub\n---\n\n# ${name.replace(/-/g, " ")}\n\n_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy.map((r) => `[[${r}]]`).join(", ")}_\n`,
909
- { encoding: "utf-8", flag: "wx" },
910
- );
911
- fixesApplied++;
912
- } catch (err) {
913
- // Page already exists — nothing to fix. Re-throw anything else.
914
- if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
915
- }
910
+ if (gap.mentionedBy.length < 2) continue;
911
+ const parts = gap.topic.split("/");
912
+ const name =
913
+ parts.length === 1
914
+ ? parts[0]
915
+ : parts.length === 2 && parts[0] === "concepts"
916
+ ? parts[1]
917
+ : "";
918
+ if (!name || slugify(name) !== name) continue;
919
+ const pagePath = join(paths.wiki, "concepts", `${name}.md`);
920
+ mkdirSync(join(paths.wiki, "concepts"), { recursive: true });
921
+ const document = createKnowledgeDocument(
922
+ `concepts/${name}.md`,
923
+ {
924
+ type: "concept",
925
+ title: name.replace(/-/g, " "),
926
+ created: fmtDate(),
927
+ updated: fmtDate(),
928
+ status: "stub",
929
+ },
930
+ `_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy
931
+ .map((source) => `[${source}](/${source}.md)`)
932
+ .join(", ")}_`,
933
+ );
934
+ try {
935
+ writeFileSync(pagePath, serializeKnowledgeDocument(document), {
936
+ encoding: "utf8",
937
+ flag: "wx",
938
+ });
939
+ fixesApplied++;
940
+ } catch (error: unknown) {
941
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
916
942
  }
917
943
  }
918
944
  }
919
945
 
920
- writeJson(join(paths.discoveries, "gaps.json"), {
921
- gaps,
922
- generated: new Date().toISOString(),
923
- });
924
-
925
946
  const reportLines = [
926
947
  "# Wiki Lint Report",
927
948
  `Generated: ${fmtDate()}`,
@@ -931,26 +952,32 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
931
952
  `- Orphans: ${orphans}`,
932
953
  `- Missing pages: ${missingPages}`,
933
954
  `- Contradictions: ${contradictions}`,
934
- autoFix ? `- Fixes applied: ${fixesApplied}` : "",
955
+ autoFix ? `- Missing-page fixes applied: ${fixesApplied}` : "",
956
+ repair?.repaired ? `- Legacy pages repaired: ${repair.repaired}` : "",
957
+ repair?.manifestPath ? `- Repair manifest: ${repair.manifestPath}` : "",
935
958
  "",
936
959
  "## Findings",
937
- findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
960
+ findings.length ? findings.map((finding) => `- ${finding}`).join("\n") : "✅ No issues found!",
938
961
  "",
939
962
  ].filter(Boolean);
940
-
941
- const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
942
- mkdirSync(paths.outputs, { recursive: true });
943
- writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
944
-
945
- appendEvent(paths, {
946
- kind: "lint",
947
- orphans,
948
- missing_pages: missingPages,
949
- contradictions,
950
- auto_fix: autoFix,
951
- });
952
-
953
- rebuildMetadataLight(paths);
963
+ const reportPath = autoFix ? join(paths.outputs, `lint-${fmtDate()}.md`) : undefined;
964
+ if (autoFix && reportPath) {
965
+ writeJson(join(paths.discoveries, "gaps.json"), {
966
+ gaps,
967
+ generated: new Date().toISOString(),
968
+ });
969
+ mkdirSync(paths.outputs, { recursive: true });
970
+ writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf8");
971
+ appendEvent(paths, {
972
+ kind: "lint",
973
+ orphans,
974
+ missing_pages: missingPages,
975
+ contradictions,
976
+ auto_fix: true,
977
+ legacy_pages_repaired: repair?.repaired ?? 0,
978
+ });
979
+ rebuildMetadataLight(paths);
980
+ }
954
981
 
955
982
  return [
956
983
  "🧹 **LLM Wiki lint complete**",
@@ -959,10 +986,12 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
959
986
  `- Orphans: ${orphans}`,
960
987
  `- Missing: ${missingPages}`,
961
988
  `- Contradictions: ${contradictions}`,
962
- autoFix ? `- Auto-fixes: ${fixesApplied}` : "",
989
+ autoFix ? `- Missing-page fixes: ${fixesApplied}` : "",
990
+ repair?.repaired ? `- Legacy pages repaired: ${repair.repaired}` : "",
963
991
  "",
964
- `📄 Report: \`${reportPath}\``,
965
- gaps.length > 0 ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
992
+ reportPath ? `📄 Report: \`${reportPath}\`` : "",
993
+ repair?.manifestPath ? `🛟 Repair manifest: \`${repair.manifestPath}\`` : "",
994
+ gaps.length ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
966
995
  ]
967
996
  .filter(Boolean)
968
997
  .join("\n");
@@ -989,18 +1018,9 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
989
1018
  };
990
1019
  }
991
1020
 
992
- const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
993
- version: "1.0",
994
- last_updated: "",
995
- pages: {},
996
- });
997
- const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
1021
+ const status = getWikiStatus(paths);
998
1022
  const config = readJson<Record<string, unknown>>(join(paths.dotWiki, "config.json"), {});
999
-
1000
- const byType: Record<string, number> = {};
1001
- for (const entry of Object.values(registry.pages)) {
1002
- byType[entry.type] = (byType[entry.type] || 0) + 1;
1003
- }
1023
+ const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
1004
1024
 
1005
1025
  const orphanCount = Object.entries(backlinks).filter(
1006
1026
  ([, inbound]) => inbound.length === 0,
@@ -1010,23 +1030,30 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1010
1030
  });
1011
1031
 
1012
1032
  const health =
1013
- Object.keys(registry.pages).length === 0
1014
- ? "🔴 Empty"
1015
- : orphanCount > 5
1016
- ? "⚠️ Warning"
1017
- : "✅ Good";
1033
+ status.totalPages === 0 ? "🔴 Empty" : orphanCount > 5 ? "⚠️ Warning" : "✅ Good";
1034
+
1035
+ const diagLines =
1036
+ status.blockingDiagnostics.length > 0
1037
+ ? [
1038
+ "",
1039
+ "⚠️ Blocking diagnostics:",
1040
+ ...status.blockingDiagnostics.map((d) => ` - ${d.code}: ${d.message}`),
1041
+ ]
1042
+ : [];
1018
1043
 
1019
1044
  const lines = [
1020
1045
  "📊 LLM Wiki Status",
1021
1046
  "══════════════════",
1022
1047
  `Topic: ${config.topic || "Unknown"}`,
1023
1048
  `Mode: ${config.mode || "personal"}`,
1024
- `Pages: ${Object.keys(registry.pages).length}`,
1025
- ...Object.entries(byType).map(([t, c]) => ` - ${t}s: ${c}`),
1049
+ `Knowledge format: ${status.knowledgeFormat}`,
1050
+ `Pages: ${status.totalPages}`,
1051
+ ...Object.entries(status.byType).map(([t, c]) => ` - ${t}s: ${c}`),
1026
1052
  `Orphans: ${orphanCount}`,
1027
1053
  `Gaps: ${gaps.gaps?.length || 0}`,
1028
1054
  `Health: ${health}`,
1029
- `Last updated: ${registry.last_updated || "Never"}`,
1055
+ `Last updated: ${status.lastUpdated || "Never"}`,
1056
+ ...diagLines,
1030
1057
  ];
1031
1058
 
1032
1059
  return {
@@ -1034,11 +1061,13 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1034
1061
  details: {
1035
1062
  topic: config.topic,
1036
1063
  mode: config.mode,
1037
- totalPages: Object.keys(registry.pages).length,
1038
- byType,
1064
+ knowledgeFormat: status.knowledgeFormat,
1065
+ totalPages: status.totalPages,
1066
+ byType: status.byType,
1039
1067
  orphans: orphanCount,
1040
1068
  gaps: gaps.gaps?.length || 0,
1041
1069
  health,
1070
+ blockingDiagnostics: status.blockingDiagnostics,
1042
1071
  } as Record<string, unknown>,
1043
1072
  };
1044
1073
  },
@@ -1057,11 +1086,16 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1057
1086
  parameters: Type.Object({}),
1058
1087
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1059
1088
  const paths = getPaths(ctx.cwd);
1060
- const vaultCheck = requireVault(paths);
1089
+ const vaultCheck = inspectWritableVault(paths);
1061
1090
  if (!vaultCheck.ok) {
1062
1091
  return {
1063
- content: [{ type: "text", text: vaultCheck.reason }],
1064
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1092
+ content: [
1093
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1094
+ ],
1095
+ details: {
1096
+ error: vaultCheck.diagnostics[0].code,
1097
+ diagnostics: vaultCheck.diagnostics,
1098
+ } as Record<string, unknown>,
1065
1099
  isError: true,
1066
1100
  };
1067
1101
  }
@@ -1073,8 +1107,11 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1073
1107
  started:
1074
1108
  "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
1075
1109
  work: async () => {
1076
- rebuildMetadata(paths);
1077
- appendEvent(paths, { kind: "rebuild_meta" });
1110
+ const result = rebuildMetadata(paths);
1111
+ // No rebuild_meta event — rebuild is a projection, not an authoritative mutation
1112
+ if (!result.ok) {
1113
+ return `⚠️ LLM Wiki: rebuild had issues — ${result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; ")}`;
1114
+ }
1078
1115
  const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
1079
1116
  version: "1.0",
1080
1117
  last_updated: "",
@@ -1109,11 +1146,16 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
1109
1146
  }),
1110
1147
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1111
1148
  const paths = getPaths(ctx.cwd);
1112
- const vaultCheck = requireVault(paths);
1149
+ const vaultCheck = inspectWritableVault(paths);
1113
1150
  if (!vaultCheck.ok) {
1114
1151
  return {
1115
- content: [{ type: "text", text: vaultCheck.reason }],
1116
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1152
+ content: [
1153
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1154
+ ],
1155
+ details: {
1156
+ error: vaultCheck.diagnostics[0].code,
1157
+ diagnostics: vaultCheck.diagnostics,
1158
+ } as Record<string, unknown>,
1117
1159
  isError: true,
1118
1160
  };
1119
1161
  }
@@ -1167,25 +1209,45 @@ export function registerWikiLogEvent(pi: ExtensionAPI): void {
1167
1209
  }),
1168
1210
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1169
1211
  const paths = getPaths(ctx.cwd);
1170
- const vaultCheck = requireVault(paths);
1212
+ const vaultCheck = inspectWritableVault(paths);
1171
1213
  if (!vaultCheck.ok) {
1172
1214
  return {
1173
- content: [{ type: "text", text: vaultCheck.reason }],
1174
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1215
+ content: [
1216
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1217
+ ],
1218
+ details: {
1219
+ error: vaultCheck.diagnostics[0].code,
1220
+ diagnostics: vaultCheck.diagnostics,
1221
+ } as Record<string, unknown>,
1175
1222
  isError: true,
1176
1223
  };
1177
1224
  }
1178
1225
 
1179
- appendEvent(paths, { kind: params.kind, ...params.details });
1226
+ const kind = typeof params.kind === "string" ? params.kind.trim() : "";
1227
+ if (!kind) {
1228
+ return {
1229
+ content: [{ type: "text", text: "Event kind must be a non-empty string" }],
1230
+ details: { error: "event_missing_kind" } as Record<string, unknown>,
1231
+ isError: true,
1232
+ };
1233
+ }
1234
+ const details = params.details ?? {};
1235
+ if (Object.hasOwn(details, "kind") || Object.hasOwn(details, "timestamp")) {
1236
+ return {
1237
+ content: [{ type: "text", text: "Event details cannot override kind or timestamp" }],
1238
+ details: { error: "event_reserved_field" } as Record<string, unknown>,
1239
+ isError: true,
1240
+ };
1241
+ }
1180
1242
 
1181
- // Regenerate log.md
1182
- const { buildLogMarkdown } = await import("./metadata.js");
1183
- const log = buildLogMarkdown(paths);
1184
- writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
1243
+ appendEvent(paths, { kind, ...details });
1244
+
1245
+ // Regenerate projections
1246
+ rebuildMetadata(paths);
1185
1247
 
1186
1248
  return {
1187
- content: [{ type: "text", text: `✅ Event logged: ${params.kind}` }],
1188
- details: { kind: params.kind } as Record<string, unknown>,
1249
+ content: [{ type: "text", text: `✅ Event logged: ${kind}` }],
1250
+ details: { kind } as Record<string, unknown>,
1189
1251
  };
1190
1252
  },
1191
1253
  });