@zosmaai/pi-llm-wiki 0.10.9 → 0.11.1

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 (82) hide show
  1. package/CHANGELOG.md +6 -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 +41 -12
  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 +74 -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 +410 -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 +505 -0
  23. package/dist/extensions/llm-wiki/lib/model-command.js +85 -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 +187 -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 +201 -0
  32. package/dist/extensions/llm-wiki/lib/tools.js +1199 -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 +353 -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/api.md +5 -2
  44. package/docs/architecture.md +5 -2
  45. package/docs/configuration.md +25 -0
  46. package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
  47. package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
  48. package/docs/superpowers/plans/2026-08-06-authoritative-event-history-phase-1-foundation-hardening.md +937 -0
  49. package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
  50. package/docs/superpowers/plans/2026-08-07-synthesis-language.md +98 -0
  51. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +593 -0
  52. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +542 -0
  53. package/docs/superpowers/specs/2026-08-07-synthesis-language-design.md +94 -0
  54. package/extensions/llm-wiki/index.ts +22 -36
  55. package/extensions/llm-wiki/lib/bootstrap.ts +87 -0
  56. package/extensions/llm-wiki/lib/embeddings.ts +9 -3
  57. package/extensions/llm-wiki/lib/guardrails.ts +26 -18
  58. package/extensions/llm-wiki/lib/indexing.ts +2 -1
  59. package/extensions/llm-wiki/lib/ingest-worker.ts +304 -28
  60. package/extensions/llm-wiki/lib/knowledge-document.ts +663 -0
  61. package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
  62. package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
  63. package/extensions/llm-wiki/lib/metadata.ts +550 -128
  64. package/extensions/llm-wiki/lib/model-command.ts +0 -1
  65. package/extensions/llm-wiki/lib/observation.ts +37 -43
  66. package/extensions/llm-wiki/lib/recall.ts +61 -33
  67. package/extensions/llm-wiki/lib/retro.ts +65 -41
  68. package/extensions/llm-wiki/lib/runtime.ts +0 -3
  69. package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
  70. package/extensions/llm-wiki/lib/source-packet.ts +45 -32
  71. package/extensions/llm-wiki/lib/task-config.ts +36 -0
  72. package/extensions/llm-wiki/lib/tools.ts +413 -342
  73. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  74. package/extensions/llm-wiki/lib/utils.ts +127 -131
  75. package/extensions/llm-wiki/lib/vault-format.ts +363 -0
  76. package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
  77. package/mcp/exec.ts +122 -0
  78. package/mcp/index.ts +60 -250
  79. package/mcp/operations.ts +176 -0
  80. package/package.json +8 -2
  81. package/scripts/migrate-llm-wiki.js +801 -0
  82. package/skills/llm-wiki/SKILL.md +12 -8
@@ -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
  }
@@ -422,6 +414,7 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
422
414
  sourceId: s.id,
423
415
  manifest: s.manifest,
424
416
  extracted: s.extracted,
417
+ synthesisLanguage: runtime.config.synthesisLanguage,
425
418
  });
426
419
  if (committed) {
427
420
  // Background semantic embeddings (#66): embed the pages this
@@ -528,11 +521,16 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
528
521
  }),
529
522
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
530
523
  const paths = getPaths(ctx.cwd);
531
- const vaultCheck = requireVault(paths);
524
+ const vaultCheck = inspectWritableVault(paths);
532
525
  if (!vaultCheck.ok) {
533
526
  return {
534
- content: [{ type: "text", text: vaultCheck.reason }],
535
- details: { error: vaultCheck.reason } as Record<string, unknown>,
527
+ content: [
528
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
529
+ ],
530
+ details: {
531
+ error: vaultCheck.diagnostics[0].code,
532
+ diagnostics: vaultCheck.diagnostics,
533
+ } as Record<string, unknown>,
536
534
  isError: true,
537
535
  };
538
536
  }
@@ -567,9 +565,19 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
567
565
  }
568
566
 
569
567
  const today = fmtDate();
570
- const template = buildPageTemplate(type, params.title, today, params.content);
568
+ const body = params.content ?? buildPageBody(type, params.title);
569
+ const doc = createKnowledgeDocument(
570
+ `${folder}/${slug}.md`,
571
+ {
572
+ type,
573
+ title: params.title,
574
+ created: today,
575
+ updated: today,
576
+ },
577
+ body,
578
+ );
571
579
  mkdirSync(join(paths.wiki, folder), { recursive: true });
572
- writeFileSync(pagePath, template, "utf-8");
580
+ writeKnowledgeDocumentFile(pagePath, doc);
573
581
 
574
582
  appendEvent(paths, {
575
583
  kind: "ensure_page",
@@ -595,151 +603,135 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
595
603
  });
596
604
  }
597
605
 
598
- function buildPageTemplate(
599
- type: string,
600
- title: string,
601
- date: string,
602
- customContent?: string,
603
- ): string {
604
- if (customContent) return customContent;
606
+ function buildPageBody(type: string, title: string): string {
607
+ if (type === "entity") {
608
+ return `# ${title}
609
+
610
+ One-line description.
605
611
 
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`;
612
+ ## Overview
607
613
 
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");
614
+ [Key facts]
615
+
616
+ ## Links
617
+
618
+ - [related-page](/concepts/related-page.md)`;
612
619
  }
613
620
  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");
621
+ return `# ${title}
622
+
623
+ One-line definition.
624
+
625
+ ## Definition
626
+
627
+ [Clear explanation]
628
+
629
+ ## Links
630
+
631
+ - [related-page](/concepts/related-page.md)`;
620
632
  }
621
633
  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");
634
+ return `# ${title}
635
+
636
+ Cross-cutting analysis.
637
+
638
+ ## Question
639
+
640
+ [What drove this?]
641
+
642
+ ## Links
643
+
644
+ - [related-page](/concepts/related-page.md)`;
628
645
  }
629
646
  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
- );
647
+ return `# ${title}
648
+
649
+ Durable answer from a query.
650
+
651
+ ## Question
652
+
653
+ [Original question]
654
+
655
+ ## Links
656
+
657
+ - [related-page](/concepts/related-page.md)`;
634
658
  }
635
659
  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");
660
+ return `# ${title}
661
+
662
+ _One-line summary of the reusable pattern this skill captures._
663
+
664
+ ## When to Use
665
+
666
+ [Trigger conditions — when this pattern applies]
667
+
668
+ ## Procedure
669
+
670
+ 1. [Step 1]
671
+ 2. [Step 2]
672
+
673
+ ## Pitfalls
674
+
675
+ - [Known failure mode or caveat]
676
+
677
+ ## Distilled From
678
+
679
+ _Trajectories this skill was generalized from._
680
+
681
+ - [trajectories/TRJ-...](/trajectories/TRJ-....md)`;
670
682
  }
671
683
  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");
684
+ return `# ${title}
685
+
686
+ _One-line summary of the specific task this case records._
687
+
688
+ ## Task
689
+
690
+ [What was requested]
691
+
692
+ ## Approach
693
+
694
+ [How the agent solved it — key steps and decisions]
695
+
696
+ ## Outcome
697
+
698
+ [Result, and anything worth reusing or avoiding next time]
699
+
700
+ ## Trajectory
701
+
702
+ - [trajectories/TRJ-...](/trajectories/TRJ-....md) — captured tool-call run`;
704
703
  }
705
704
  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");
705
+ return `# ${title}
706
+
707
+ ## Description
708
+
709
+ [Clear description of what this requirement entails]
710
+
711
+ ## Acceptance Criteria
712
+
713
+ - [ ] [Criterion 1]
714
+ - [ ] [Criterion 2]
715
+
716
+ ## Dependencies
717
+
718
+ _Pages this requirement depends on._
719
+
720
+ ## Implementation Notes
721
+
722
+ [Optional notes]
723
+
724
+ ## Sources
725
+
726
+ - [sources/SRC-...](/sources/SRC-....md) — original concept capture`;
741
727
  }
742
- return base;
728
+ return `# ${title}
729
+
730
+ [Description to be filled]
731
+
732
+ ## Links
733
+
734
+ - [related-page](/concepts/related-page.md)`;
743
735
  }
744
736
 
745
737
  // ─── 5. wiki_search ─────────────────────────────────────
@@ -757,29 +749,15 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
757
749
  }),
758
750
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
759
751
  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) {
752
+ const result = searchRegistry(paths, params.query, params.type);
753
+
754
+ if (result.matches.length === 0) {
780
755
  return {
781
756
  content: [{ type: "text", text: `No pages found for "${params.query}"` }],
782
- details: { query: params.query, matches: [] } as Record<string, unknown>,
757
+ details: { query: params.query, matches: [], diagnostics: result.diagnostics } as Record<
758
+ string,
759
+ unknown
760
+ >,
783
761
  };
784
762
  }
785
763
 
@@ -788,13 +766,17 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
788
766
  {
789
767
  type: "text",
790
768
  text: [
791
- `🔍 **${matches.length} result(s)** for "${params.query}":`,
769
+ `🔍 **${result.matches.length} result(s)** for "${params.query}":`,
792
770
  "",
793
- ...matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
771
+ ...result.matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
794
772
  ].join("\n"),
795
773
  },
796
774
  ],
797
- details: { query: params.query, matches } as Record<string, unknown>,
775
+ details: {
776
+ query: params.query,
777
+ matches: result.matches,
778
+ diagnostics: result.diagnostics,
779
+ } as Record<string, unknown>,
798
780
  };
799
781
  },
800
782
  });
@@ -820,11 +802,16 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
820
802
  }),
821
803
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
822
804
  const paths = getPaths(ctx.cwd);
823
- const vaultCheck = requireVault(paths);
805
+ const vaultCheck = inspectWritableVault(paths);
824
806
  if (!vaultCheck.ok) {
825
807
  return {
826
- content: [{ type: "text", text: vaultCheck.reason }],
827
- details: { error: vaultCheck.reason } as Record<string, unknown>,
808
+ content: [
809
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
810
+ ],
811
+ details: {
812
+ error: vaultCheck.diagnostics[0].code,
813
+ diagnostics: vaultCheck.diagnostics,
814
+ } as Record<string, unknown>,
828
815
  isError: true,
829
816
  };
830
817
  }
@@ -846,82 +833,117 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
846
833
  * run off-thread via `dispatchReported`). Returns the human-readable summary.
847
834
  */
848
835
  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
836
+ assertWritableVault(paths);
837
+ let repair: ReturnType<typeof repairLegacyKnowledgeDocuments> | undefined;
838
+ if (autoFix) {
839
+ let projection = rebuildMetadata(paths);
840
+ repair = !projection.ok ? repairLegacyKnowledgeDocuments(paths) : undefined;
841
+ if (repair?.repaired) projection = rebuildMetadata(paths);
842
+ if (!projection.ok) {
843
+ return [
844
+ "# Wiki Lint Report",
845
+ "",
846
+ repair?.repaired ? `Legacy pages repaired: ${repair.repaired}` : "",
847
+ repair?.manifestPath ? `Repair manifest: ${repair.manifestPath}` : "",
848
+ "Projection-blocking diagnostics:",
849
+ ...projection.diagnostics.map(
850
+ (diagnostic) => `- ${diagnostic.code}: ${diagnostic.path}: ${diagnostic.message}`,
851
+ ),
852
+ ]
853
+ .filter(Boolean)
854
+ .join("\n");
855
+ }
856
+ } else {
857
+ const vault = inspectVaultFormat(paths);
858
+ const audit = discoverKnowledgeDocuments(paths);
859
+ const diagnostics = [...vault.diagnostics, ...audit.diagnostics];
860
+ if (vault.blocking || audit.blocking) {
861
+ return [
862
+ "# Wiki Lint Report",
863
+ "",
864
+ "Projection-blocking diagnostics:",
865
+ ...diagnostics.map(
866
+ (diagnostic) => `- ${diagnostic.code}: ${diagnostic.path}: ${diagnostic.message}`,
867
+ ),
868
+ ].join("\n");
869
+ }
870
+ }
852
871
 
872
+ const discovery = discoverKnowledgeDocuments(paths);
873
+ const pages = discovery.documents;
874
+ const knownIds = new Set(pages.map((page) => page.id));
875
+ const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
876
+ const gapSources = new Map<string, Set<string>>();
853
877
  const findings: string[] = [];
854
- let orphans = 0;
855
878
  let missingPages = 0;
856
879
  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
880
 
862
881
  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
- }
882
+ const resolved = buildResolvedBacklinks(page.id, page.body, knownIds);
883
+ for (const target of resolved.targets) inbound[target]++;
884
+ for (const unresolved of resolved.unresolved) {
885
+ const sources = gapSources.get(unresolved.target) ?? new Set<string>();
886
+ sources.add(page.id);
887
+ gapSources.set(unresolved.target, sources);
888
+ missingPages++;
889
+ findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
878
890
  }
879
891
  }
880
892
 
893
+ let orphans = 0;
881
894
  for (const page of pages) {
882
- if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
895
+ if (inbound[page.id] === 0) {
883
896
  orphans++;
884
- findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
897
+ findings.push(`Orphan: ${page.id} has no inbound links`);
885
898
  }
886
- }
887
-
888
- for (const page of pages) {
889
- if (page.content.includes("⚠️ **Contradiction")) {
899
+ if (page.body.includes("⚠️ **Contradiction")) {
890
900
  contradictions++;
891
- findings.push(`Contradiction flagged in [[${page.relative}]]`);
901
+ findings.push(`Contradiction flagged in ${page.id}`);
892
902
  }
893
903
  }
894
904
 
905
+ const gaps = [...gapSources.entries()]
906
+ .map(([topic, sources]) => ({ topic, mentionedBy: [...sources].sort(compareCodePoint) }))
907
+ .sort((left, right) => compareCodePoint(left.topic, right.topic));
895
908
  let fixesApplied = 0;
896
909
  if (autoFix) {
897
910
  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
- }
911
+ if (gap.mentionedBy.length < 2) continue;
912
+ const parts = gap.topic.split("/");
913
+ const name =
914
+ parts.length === 1
915
+ ? parts[0]
916
+ : parts.length === 2 && parts[0] === "concepts"
917
+ ? parts[1]
918
+ : "";
919
+ if (!name || slugify(name) !== name) continue;
920
+ const pagePath = join(paths.wiki, "concepts", `${name}.md`);
921
+ mkdirSync(join(paths.wiki, "concepts"), { recursive: true });
922
+ const document = createKnowledgeDocument(
923
+ `concepts/${name}.md`,
924
+ {
925
+ type: "concept",
926
+ title: name.replace(/-/g, " "),
927
+ created: fmtDate(),
928
+ updated: fmtDate(),
929
+ status: "stub",
930
+ },
931
+ `_Stub auto-created by lint. Expand with content from: ${gap.mentionedBy
932
+ .map((source) => `[${source}](/${source}.md)`)
933
+ .join(", ")}_`,
934
+ );
935
+ try {
936
+ writeFileSync(pagePath, serializeKnowledgeDocument(document), {
937
+ encoding: "utf8",
938
+ flag: "wx",
939
+ });
940
+ fixesApplied++;
941
+ } catch (error: unknown) {
942
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
916
943
  }
917
944
  }
918
945
  }
919
946
 
920
- writeJson(join(paths.discoveries, "gaps.json"), {
921
- gaps,
922
- generated: new Date().toISOString(),
923
- });
924
-
925
947
  const reportLines = [
926
948
  "# Wiki Lint Report",
927
949
  `Generated: ${fmtDate()}`,
@@ -931,26 +953,32 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
931
953
  `- Orphans: ${orphans}`,
932
954
  `- Missing pages: ${missingPages}`,
933
955
  `- Contradictions: ${contradictions}`,
934
- autoFix ? `- Fixes applied: ${fixesApplied}` : "",
956
+ autoFix ? `- Missing-page fixes applied: ${fixesApplied}` : "",
957
+ repair?.repaired ? `- Legacy pages repaired: ${repair.repaired}` : "",
958
+ repair?.manifestPath ? `- Repair manifest: ${repair.manifestPath}` : "",
935
959
  "",
936
960
  "## Findings",
937
- findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
961
+ findings.length ? findings.map((finding) => `- ${finding}`).join("\n") : "✅ No issues found!",
938
962
  "",
939
963
  ].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);
964
+ const reportPath = autoFix ? join(paths.outputs, `lint-${fmtDate()}.md`) : undefined;
965
+ if (autoFix && reportPath) {
966
+ writeJson(join(paths.discoveries, "gaps.json"), {
967
+ gaps,
968
+ generated: new Date().toISOString(),
969
+ });
970
+ mkdirSync(paths.outputs, { recursive: true });
971
+ writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf8");
972
+ appendEvent(paths, {
973
+ kind: "lint",
974
+ orphans,
975
+ missing_pages: missingPages,
976
+ contradictions,
977
+ auto_fix: true,
978
+ legacy_pages_repaired: repair?.repaired ?? 0,
979
+ });
980
+ rebuildMetadataLight(paths);
981
+ }
954
982
 
955
983
  return [
956
984
  "🧹 **LLM Wiki lint complete**",
@@ -959,10 +987,12 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
959
987
  `- Orphans: ${orphans}`,
960
988
  `- Missing: ${missingPages}`,
961
989
  `- Contradictions: ${contradictions}`,
962
- autoFix ? `- Auto-fixes: ${fixesApplied}` : "",
990
+ autoFix ? `- Missing-page fixes: ${fixesApplied}` : "",
991
+ repair?.repaired ? `- Legacy pages repaired: ${repair.repaired}` : "",
963
992
  "",
964
- `📄 Report: \`${reportPath}\``,
965
- gaps.length > 0 ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
993
+ reportPath ? `📄 Report: \`${reportPath}\`` : "",
994
+ repair?.manifestPath ? `🛟 Repair manifest: \`${repair.manifestPath}\`` : "",
995
+ gaps.length ? `💡 ${gaps.length} knowledge gap(s) tracked` : "",
966
996
  ]
967
997
  .filter(Boolean)
968
998
  .join("\n");
@@ -989,18 +1019,9 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
989
1019
  };
990
1020
  }
991
1021
 
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"), {});
1022
+ const status = getWikiStatus(paths);
998
1023
  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
- }
1024
+ const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
1004
1025
 
1005
1026
  const orphanCount = Object.entries(backlinks).filter(
1006
1027
  ([, inbound]) => inbound.length === 0,
@@ -1010,23 +1031,30 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1010
1031
  });
1011
1032
 
1012
1033
  const health =
1013
- Object.keys(registry.pages).length === 0
1014
- ? "🔴 Empty"
1015
- : orphanCount > 5
1016
- ? "⚠️ Warning"
1017
- : "✅ Good";
1034
+ status.totalPages === 0 ? "🔴 Empty" : orphanCount > 5 ? "⚠️ Warning" : "✅ Good";
1035
+
1036
+ const diagLines =
1037
+ status.blockingDiagnostics.length > 0
1038
+ ? [
1039
+ "",
1040
+ "⚠️ Blocking diagnostics:",
1041
+ ...status.blockingDiagnostics.map((d) => ` - ${d.code}: ${d.message}`),
1042
+ ]
1043
+ : [];
1018
1044
 
1019
1045
  const lines = [
1020
1046
  "📊 LLM Wiki Status",
1021
1047
  "══════════════════",
1022
1048
  `Topic: ${config.topic || "Unknown"}`,
1023
1049
  `Mode: ${config.mode || "personal"}`,
1024
- `Pages: ${Object.keys(registry.pages).length}`,
1025
- ...Object.entries(byType).map(([t, c]) => ` - ${t}s: ${c}`),
1050
+ `Knowledge format: ${status.knowledgeFormat}`,
1051
+ `Pages: ${status.totalPages}`,
1052
+ ...Object.entries(status.byType).map(([t, c]) => ` - ${t}s: ${c}`),
1026
1053
  `Orphans: ${orphanCount}`,
1027
1054
  `Gaps: ${gaps.gaps?.length || 0}`,
1028
1055
  `Health: ${health}`,
1029
- `Last updated: ${registry.last_updated || "Never"}`,
1056
+ `Last updated: ${status.lastUpdated || "Never"}`,
1057
+ ...diagLines,
1030
1058
  ];
1031
1059
 
1032
1060
  return {
@@ -1034,11 +1062,13 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1034
1062
  details: {
1035
1063
  topic: config.topic,
1036
1064
  mode: config.mode,
1037
- totalPages: Object.keys(registry.pages).length,
1038
- byType,
1065
+ knowledgeFormat: status.knowledgeFormat,
1066
+ totalPages: status.totalPages,
1067
+ byType: status.byType,
1039
1068
  orphans: orphanCount,
1040
1069
  gaps: gaps.gaps?.length || 0,
1041
1070
  health,
1071
+ blockingDiagnostics: status.blockingDiagnostics,
1042
1072
  } as Record<string, unknown>,
1043
1073
  };
1044
1074
  },
@@ -1057,11 +1087,16 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1057
1087
  parameters: Type.Object({}),
1058
1088
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1059
1089
  const paths = getPaths(ctx.cwd);
1060
- const vaultCheck = requireVault(paths);
1090
+ const vaultCheck = inspectWritableVault(paths);
1061
1091
  if (!vaultCheck.ok) {
1062
1092
  return {
1063
- content: [{ type: "text", text: vaultCheck.reason }],
1064
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1093
+ content: [
1094
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1095
+ ],
1096
+ details: {
1097
+ error: vaultCheck.diagnostics[0].code,
1098
+ diagnostics: vaultCheck.diagnostics,
1099
+ } as Record<string, unknown>,
1065
1100
  isError: true,
1066
1101
  };
1067
1102
  }
@@ -1073,8 +1108,19 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1073
1108
  started:
1074
1109
  "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
1075
1110
  work: async () => {
1076
- rebuildMetadata(paths);
1077
- appendEvent(paths, { kind: "rebuild_meta" });
1111
+ const result = rebuildMetadata(paths);
1112
+ // No rebuild_meta event — rebuild is a projection, not an authoritative mutation
1113
+ if (!result.ok) {
1114
+ return `⚠️ LLM Wiki: rebuild had issues — ${result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("; ")}`;
1115
+ }
1116
+ const warnings = result.diagnostics.filter(
1117
+ (diagnostic) => diagnostic.severity === "warning",
1118
+ );
1119
+ if (warnings.length > 0) {
1120
+ return `⚠️ LLM Wiki: metadata rebuilt with warnings — ${warnings
1121
+ .map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
1122
+ .join("; ")}`;
1123
+ }
1078
1124
  const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
1079
1125
  version: "1.0",
1080
1126
  last_updated: "",
@@ -1109,11 +1155,16 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
1109
1155
  }),
1110
1156
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1111
1157
  const paths = getPaths(ctx.cwd);
1112
- const vaultCheck = requireVault(paths);
1158
+ const vaultCheck = inspectWritableVault(paths);
1113
1159
  if (!vaultCheck.ok) {
1114
1160
  return {
1115
- content: [{ type: "text", text: vaultCheck.reason }],
1116
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1161
+ content: [
1162
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1163
+ ],
1164
+ details: {
1165
+ error: vaultCheck.diagnostics[0].code,
1166
+ diagnostics: vaultCheck.diagnostics,
1167
+ } as Record<string, unknown>,
1117
1168
  isError: true,
1118
1169
  };
1119
1170
  }
@@ -1167,25 +1218,45 @@ export function registerWikiLogEvent(pi: ExtensionAPI): void {
1167
1218
  }),
1168
1219
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1169
1220
  const paths = getPaths(ctx.cwd);
1170
- const vaultCheck = requireVault(paths);
1221
+ const vaultCheck = inspectWritableVault(paths);
1171
1222
  if (!vaultCheck.ok) {
1172
1223
  return {
1173
- content: [{ type: "text", text: vaultCheck.reason }],
1174
- details: { error: vaultCheck.reason } as Record<string, unknown>,
1224
+ content: [
1225
+ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
1226
+ ],
1227
+ details: {
1228
+ error: vaultCheck.diagnostics[0].code,
1229
+ diagnostics: vaultCheck.diagnostics,
1230
+ } as Record<string, unknown>,
1175
1231
  isError: true,
1176
1232
  };
1177
1233
  }
1178
1234
 
1179
- appendEvent(paths, { kind: params.kind, ...params.details });
1235
+ const kind = typeof params.kind === "string" ? params.kind.trim() : "";
1236
+ if (!kind) {
1237
+ return {
1238
+ content: [{ type: "text", text: "Event kind must be a non-empty string" }],
1239
+ details: { error: "event_missing_kind" } as Record<string, unknown>,
1240
+ isError: true,
1241
+ };
1242
+ }
1243
+ const details = params.details ?? {};
1244
+ if (Object.hasOwn(details, "kind") || Object.hasOwn(details, "timestamp")) {
1245
+ return {
1246
+ content: [{ type: "text", text: "Event details cannot override kind or timestamp" }],
1247
+ details: { error: "event_reserved_field" } as Record<string, unknown>,
1248
+ isError: true,
1249
+ };
1250
+ }
1180
1251
 
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");
1252
+ appendEvent(paths, { kind, ...details });
1253
+
1254
+ // Regenerate projections
1255
+ rebuildMetadata(paths);
1185
1256
 
1186
1257
  return {
1187
- content: [{ type: "text", text: `✅ Event logged: ${params.kind}` }],
1188
- details: { kind: params.kind } as Record<string, unknown>,
1258
+ content: [{ type: "text", text: `✅ Event logged: ${kind}` }],
1259
+ details: { kind } as Record<string, unknown>,
1189
1260
  };
1190
1261
  },
1191
1262
  });