@zosmaai/pi-llm-wiki 0.10.7 → 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 +38 -3
  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 +174 -29
  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 +406 -348
  64. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  65. package/extensions/llm-wiki/lib/utils.ts +121 -130
  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,32 +2,39 @@ 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,
28
26
  resolveVaultPaths,
27
+ slugify,
29
28
  writeJson,
30
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";
31
38
 
32
39
  /**
33
40
  * All LLM Wiki custom tools.
@@ -115,59 +122,35 @@ export function registerWikiBootstrap(pi: ExtensionAPI): void {
115
122
  const root = params.root ?? ctx.cwd ?? process.cwd();
116
123
  const mode = params.mode || "personal";
117
124
  const paths = getVaultPaths(root);
118
-
119
- ensureVaultStructure(paths);
120
-
121
- const config = {
122
- name: params.topic,
123
- mode,
124
- topic: params.topic,
125
- created: fmtDate(),
126
- version: "1.0",
127
- };
128
- writeJson(join(paths.dotWiki, "config.json"), config);
129
-
130
- const schema = [
131
- "# LLM Wiki Schema",
132
- "",
133
- "## Ownership Rules",
134
- "",
135
- "| Path | Owner | Rule |",
136
- "|------|-------|------|",
137
- "| raw/** | extension | immutable after capture |",
138
- "| wiki/** | model + user | editable knowledge pages |",
139
- "| meta/* | extension | auto-generated |",
140
- "| . | human + explicit request | operating rules |",
141
- "",
142
- "## Source Packet Format",
143
- "",
144
- "```",
145
- "raw/sources/SRC-YYYY-MM-DD-NNN/",
146
- " manifest.json",
147
- " original/",
148
- " extracted.md",
149
- " attachments/",
150
- "```",
151
- "",
152
- "## Page Types",
153
- "",
154
- "- **source** — what this specific source says",
155
- "- **entity** — people, orgs, tools, products",
156
- "- **concept** — ideas, patterns, frameworks",
157
- "- **synthesis** — cross-source theses and tensions",
158
- "- **analysis** — durable filed answers from queries",
159
- "- **requirement** — atomic requirements with status, priority, and traceability",
160
- "",
161
- "## Linking Style",
162
- "",
163
- "- Internal: [[folder/page-name]]",
164
- "- Citation: [[sources/SRC-YYYY-MM-DD-NNN]]",
165
- "",
166
- ].join("\n");
167
- writeFileSync(join(paths.dotWiki, "WIKI_SCHEMA.md"), schema, "utf-8");
168
-
169
- rebuildMetadata(paths);
170
- 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
+ }
171
154
 
172
155
  return {
173
156
  content: [
@@ -215,11 +198,16 @@ export function registerWikiCaptureSource(pi: ExtensionAPI, runtime?: Runtime):
215
198
  }),
216
199
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
217
200
  const paths = getPaths(ctx.cwd);
218
- const vaultCheck = requireVault(paths);
201
+ const vaultCheck = inspectWritableVault(paths);
219
202
  if (!vaultCheck.ok) {
220
203
  return {
221
- content: [{ type: "text", text: vaultCheck.reason }],
222
- 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>,
223
211
  isError: true,
224
212
  };
225
213
  }
@@ -314,11 +302,16 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
314
302
  }),
315
303
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
316
304
  const paths = getPaths(ctx.cwd);
317
- const vaultCheck = requireVault(paths);
305
+ const vaultCheck = inspectWritableVault(paths);
318
306
  if (!vaultCheck.ok) {
319
307
  return {
320
- content: [{ type: "text", text: vaultCheck.reason }],
321
- 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>,
322
315
  isError: true,
323
316
  };
324
317
  }
@@ -527,11 +520,16 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
527
520
  }),
528
521
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
529
522
  const paths = getPaths(ctx.cwd);
530
- const vaultCheck = requireVault(paths);
523
+ const vaultCheck = inspectWritableVault(paths);
531
524
  if (!vaultCheck.ok) {
532
525
  return {
533
- content: [{ type: "text", text: vaultCheck.reason }],
534
- 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>,
535
533
  isError: true,
536
534
  };
537
535
  }
@@ -544,12 +542,7 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
544
542
  | "requirement"
545
543
  | "skill"
546
544
  | "case";
547
- const slug = params.title
548
- .toLowerCase()
549
- .replace(/[^a-z0-9\s-]/g, "")
550
- .trim()
551
- .replace(/\s+/g, "-")
552
- .slice(0, 80);
545
+ const slug = slugify(params.title);
553
546
 
554
547
  const folderMap: Record<string, string> = {
555
548
  entity: "entities",
@@ -571,9 +564,19 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
571
564
  }
572
565
 
573
566
  const today = fmtDate();
574
- 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
+ );
575
578
  mkdirSync(join(paths.wiki, folder), { recursive: true });
576
- writeFileSync(pagePath, template, "utf-8");
579
+ writeKnowledgeDocumentFile(pagePath, doc);
577
580
 
578
581
  appendEvent(paths, {
579
582
  kind: "ensure_page",
@@ -599,151 +602,135 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
599
602
  });
600
603
  }
601
604
 
602
- function buildPageTemplate(
603
- type: string,
604
- title: string,
605
- date: string,
606
- customContent?: string,
607
- ): string {
608
- if (customContent) return customContent;
605
+ function buildPageBody(type: string, title: string): string {
606
+ if (type === "entity") {
607
+ return `# ${title}
608
+
609
+ One-line description.
609
610
 
610
- 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
611
612
 
612
- if (type === "entity") {
613
- return base
614
- .replace("[Description to be filled]", "One-line description.\n\n## Overview\n\n[Key facts]")
615
- .replace("type: entity", "type: entity\ncategory: organization");
613
+ [Key facts]
614
+
615
+ ## Links
616
+
617
+ - [related-page](/concepts/related-page.md)`;
616
618
  }
617
619
  if (type === "concept") {
618
- return base
619
- .replace(
620
- "[Description to be filled]",
621
- "One-line definition.\n\n## Definition\n\n[Clear explanation]",
622
- )
623
- .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)`;
624
631
  }
625
632
  if (type === "synthesis") {
626
- return base
627
- .replace(
628
- "[Description to be filled]",
629
- "Cross-cutting analysis.\n\n## Question\n\n[What drove this?]",
630
- )
631
- .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)`;
632
644
  }
633
645
  if (type === "analysis") {
634
- return base.replace(
635
- "[Description to be filled]",
636
- "Durable answer from a query.\n\n## Question\n\n[Original question]",
637
- );
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)`;
638
657
  }
639
658
  if (type === "skill") {
640
- return [
641
- "---",
642
- "type: skill",
643
- `created: ${date}`,
644
- `updated: ${date}`,
645
- "status: draft",
646
- "trajectories: []",
647
- "tags: []",
648
- "---",
649
- "",
650
- `# ${title}`,
651
- "",
652
- "_One-line summary of the reusable pattern this skill captures._",
653
- "",
654
- "## When to Use",
655
- "",
656
- "[Trigger conditions — when this pattern applies]",
657
- "",
658
- "## Procedure",
659
- "",
660
- "1. [Step 1]",
661
- "2. [Step 2]",
662
- "",
663
- "## Pitfalls",
664
- "",
665
- "- [Known failure mode or caveat]",
666
- "",
667
- "## Distilled From",
668
- "",
669
- "_Trajectories this skill was generalized from._",
670
- "",
671
- "- [[trajectories/TRJ-...]]",
672
- "",
673
- ].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)`;
674
681
  }
675
682
  if (type === "case") {
676
- return [
677
- "---",
678
- "type: case",
679
- `created: ${date}`,
680
- `updated: ${date}`,
681
- "status: draft",
682
- "outcome: success",
683
- "trajectory_id: ",
684
- "tags: []",
685
- "---",
686
- "",
687
- `# ${title}`,
688
- "",
689
- "_One-line summary of the specific task this case records._",
690
- "",
691
- "## Task",
692
- "",
693
- "[What was requested]",
694
- "",
695
- "## Approach",
696
- "",
697
- "[How the agent solved it — key steps and decisions]",
698
- "",
699
- "## Outcome",
700
- "",
701
- "[Result, and anything worth reusing or avoiding next time]",
702
- "",
703
- "## Trajectory",
704
- "",
705
- "- [[trajectories/TRJ-...]] — captured tool-call run",
706
- "",
707
- ].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`;
708
702
  }
709
703
  if (type === "requirement") {
710
- return [
711
- "---",
712
- "type: requirement",
713
- `created: ${date}`,
714
- `updated: ${date}`,
715
- "status: draft",
716
- "priority: p2",
717
- "source_id: ",
718
- "depends_on: []",
719
- "---",
720
- "",
721
- `# ${title}`,
722
- "",
723
- "## Description",
724
- "",
725
- "[Clear description of what this requirement entails]",
726
- "",
727
- "## Acceptance Criteria",
728
- "",
729
- "- [ ] [Criterion 1]",
730
- "- [ ] [Criterion 2]",
731
- "",
732
- "## Dependencies",
733
- "",
734
- "_Pages this requirement depends on._",
735
- "",
736
- "## Implementation Notes",
737
- "",
738
- "[Optional notes]",
739
- "",
740
- "## Sources",
741
- "",
742
- "- [[sources/SRC-...]] — original concept capture",
743
- "",
744
- ].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`;
745
726
  }
746
- return base;
727
+ return `# ${title}
728
+
729
+ [Description to be filled]
730
+
731
+ ## Links
732
+
733
+ - [related-page](/concepts/related-page.md)`;
747
734
  }
748
735
 
749
736
  // ─── 5. wiki_search ─────────────────────────────────────
@@ -761,29 +748,15 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
761
748
  }),
762
749
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
763
750
  const paths = getPaths(ctx.cwd);
764
- const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
765
- version: "1.0",
766
- last_updated: "",
767
- pages: {},
768
- });
769
- const q = params.query.toLowerCase();
770
-
771
- const matches = Object.entries(registry.pages)
772
- .filter(([id, entry]) => {
773
- const matchesQuery =
774
- id.toLowerCase().includes(q) ||
775
- String(entry.title).toLowerCase().includes(q) ||
776
- String(entry.type).toLowerCase().includes(q);
777
- const matchesType =
778
- !params.type || String(entry.type).toLowerCase() === params.type.toLowerCase();
779
- return matchesQuery && matchesType;
780
- })
781
- .map(([id, entry]) => ({ id, title: entry.title, type: entry.type }));
782
-
783
- if (matches.length === 0) {
751
+ const result = searchRegistry(paths, params.query, params.type);
752
+
753
+ if (result.matches.length === 0) {
784
754
  return {
785
755
  content: [{ type: "text", text: `No pages found for "${params.query}"` }],
786
- 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
+ >,
787
760
  };
788
761
  }
789
762
 
@@ -792,13 +765,17 @@ export function registerWikiSearch(pi: ExtensionAPI): void {
792
765
  {
793
766
  type: "text",
794
767
  text: [
795
- `🔍 **${matches.length} result(s)** for "${params.query}":`,
768
+ `🔍 **${result.matches.length} result(s)** for "${params.query}":`,
796
769
  "",
797
- ...matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
770
+ ...result.matches.map((m) => `- [[${m.id}]] — *${m.type}* — ${m.title}`),
798
771
  ].join("\n"),
799
772
  },
800
773
  ],
801
- 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>,
802
779
  };
803
780
  },
804
781
  });
@@ -824,11 +801,16 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
824
801
  }),
825
802
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
826
803
  const paths = getPaths(ctx.cwd);
827
- const vaultCheck = requireVault(paths);
804
+ const vaultCheck = inspectWritableVault(paths);
828
805
  if (!vaultCheck.ok) {
829
806
  return {
830
- content: [{ type: "text", text: vaultCheck.reason }],
831
- 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>,
832
814
  isError: true,
833
815
  };
834
816
  }
@@ -850,82 +832,117 @@ export function registerWikiLint(pi: ExtensionAPI, runtime?: Runtime): void {
850
832
  * run off-thread via `dispatchReported`). Returns the human-readable summary.
851
833
  */
852
834
  function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
853
- const pages = findWikiPages(paths.wiki);
854
- const registry = buildRegistry(paths);
855
- 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
+ }
856
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>>();
857
876
  const findings: string[] = [];
858
- let orphans = 0;
859
877
  let missingPages = 0;
860
878
  let contradictions = 0;
861
- const gaps: Array<{ topic: string; mentionedBy: string[] }> = [];
862
-
863
- const allPageIds = new Set(pages.map((p) => p.relative));
864
- const linkCounts: Record<string, number> = {};
865
879
 
866
880
  for (const page of pages) {
867
- const links = extractWikilinks(page.content);
868
- for (const link of links) {
869
- if (!allPageIds.has(link)) {
870
- missingPages++;
871
- findings.push(`Missing page: [[${link}]] (in [[${page.relative}]])`);
872
- const existing = gaps.find((g) => g.topic === link);
873
- if (existing) {
874
- if (!existing.mentionedBy.includes(page.relative))
875
- existing.mentionedBy.push(page.relative);
876
- } else {
877
- gaps.push({ topic: link, mentionedBy: [page.relative] });
878
- }
879
- } else {
880
- linkCounts[link] = (linkCounts[link] || 0) + 1;
881
- }
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})`);
882
889
  }
883
890
  }
884
891
 
892
+ let orphans = 0;
885
893
  for (const page of pages) {
886
- if (!linkCounts[page.relative] || linkCounts[page.relative] === 0) {
894
+ if (inbound[page.id] === 0) {
887
895
  orphans++;
888
- findings.push(`Orphan: [[${page.relative}]] has no inbound links`);
896
+ findings.push(`Orphan: ${page.id} has no inbound links`);
889
897
  }
890
- }
891
-
892
- for (const page of pages) {
893
- if (page.content.includes("⚠️ **Contradiction")) {
898
+ if (page.body.includes("⚠️ **Contradiction")) {
894
899
  contradictions++;
895
- findings.push(`Contradiction flagged in [[${page.relative}]]`);
900
+ findings.push(`Contradiction flagged in ${page.id}`);
896
901
  }
897
902
  }
898
903
 
904
+ const gaps = [...gapSources.entries()]
905
+ .map(([topic, sources]) => ({ topic, mentionedBy: [...sources].sort(compareCodePoint) }))
906
+ .sort((left, right) => compareCodePoint(left.topic, right.topic));
899
907
  let fixesApplied = 0;
900
908
  if (autoFix) {
901
909
  for (const gap of gaps) {
902
- if (gap.mentionedBy.length >= 2) {
903
- const folder = gap.topic.includes("/") ? gap.topic.split("/")[0] : "concepts";
904
- const name = gap.topic.includes("/") ? gap.topic.split("/").pop()! : gap.topic;
905
- const pagePath = join(paths.wiki, folder, `${name}.md`);
906
- mkdirSync(join(paths.wiki, folder), { recursive: true });
907
- try {
908
- // Atomic create-if-absent: the `wx` flag fails with EEXIST instead of
909
- // overwriting, avoiding the existsSync→write TOCTOU race (CodeQL).
910
- writeFileSync(
911
- pagePath,
912
- `---\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`,
913
- { encoding: "utf-8", flag: "wx" },
914
- );
915
- fixesApplied++;
916
- } catch (err) {
917
- // Page already exists — nothing to fix. Re-throw anything else.
918
- if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
919
- }
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;
920
942
  }
921
943
  }
922
944
  }
923
945
 
924
- writeJson(join(paths.discoveries, "gaps.json"), {
925
- gaps,
926
- generated: new Date().toISOString(),
927
- });
928
-
929
946
  const reportLines = [
930
947
  "# Wiki Lint Report",
931
948
  `Generated: ${fmtDate()}`,
@@ -935,26 +952,32 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
935
952
  `- Orphans: ${orphans}`,
936
953
  `- Missing pages: ${missingPages}`,
937
954
  `- Contradictions: ${contradictions}`,
938
- 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}` : "",
939
958
  "",
940
959
  "## Findings",
941
- findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "✅ No issues found!",
960
+ findings.length ? findings.map((finding) => `- ${finding}`).join("\n") : "✅ No issues found!",
942
961
  "",
943
962
  ].filter(Boolean);
944
-
945
- const reportPath = join(paths.outputs, `lint-${fmtDate()}.md`);
946
- mkdirSync(paths.outputs, { recursive: true });
947
- writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
948
-
949
- appendEvent(paths, {
950
- kind: "lint",
951
- orphans,
952
- missing_pages: missingPages,
953
- contradictions,
954
- auto_fix: autoFix,
955
- });
956
-
957
- 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
+ }
958
981
 
959
982
  return [
960
983
  "🧹 **LLM Wiki lint complete**",
@@ -963,10 +986,12 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
963
986
  `- Orphans: ${orphans}`,
964
987
  `- Missing: ${missingPages}`,
965
988
  `- Contradictions: ${contradictions}`,
966
- autoFix ? `- Auto-fixes: ${fixesApplied}` : "",
989
+ autoFix ? `- Missing-page fixes: ${fixesApplied}` : "",
990
+ repair?.repaired ? `- Legacy pages repaired: ${repair.repaired}` : "",
967
991
  "",
968
- `📄 Report: \`${reportPath}\``,
969
- 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` : "",
970
995
  ]
971
996
  .filter(Boolean)
972
997
  .join("\n");
@@ -993,18 +1018,9 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
993
1018
  };
994
1019
  }
995
1020
 
996
- const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
997
- version: "1.0",
998
- last_updated: "",
999
- pages: {},
1000
- });
1001
- const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
1021
+ const status = getWikiStatus(paths);
1002
1022
  const config = readJson<Record<string, unknown>>(join(paths.dotWiki, "config.json"), {});
1003
-
1004
- const byType: Record<string, number> = {};
1005
- for (const entry of Object.values(registry.pages)) {
1006
- byType[entry.type] = (byType[entry.type] || 0) + 1;
1007
- }
1023
+ const backlinks = readJson<Record<string, string[]>>(join(paths.meta, "backlinks.json"), {});
1008
1024
 
1009
1025
  const orphanCount = Object.entries(backlinks).filter(
1010
1026
  ([, inbound]) => inbound.length === 0,
@@ -1014,23 +1030,30 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1014
1030
  });
1015
1031
 
1016
1032
  const health =
1017
- Object.keys(registry.pages).length === 0
1018
- ? "🔴 Empty"
1019
- : orphanCount > 5
1020
- ? "⚠️ Warning"
1021
- : "✅ 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
+ : [];
1022
1043
 
1023
1044
  const lines = [
1024
1045
  "📊 LLM Wiki Status",
1025
1046
  "══════════════════",
1026
1047
  `Topic: ${config.topic || "Unknown"}`,
1027
1048
  `Mode: ${config.mode || "personal"}`,
1028
- `Pages: ${Object.keys(registry.pages).length}`,
1029
- ...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}`),
1030
1052
  `Orphans: ${orphanCount}`,
1031
1053
  `Gaps: ${gaps.gaps?.length || 0}`,
1032
1054
  `Health: ${health}`,
1033
- `Last updated: ${registry.last_updated || "Never"}`,
1055
+ `Last updated: ${status.lastUpdated || "Never"}`,
1056
+ ...diagLines,
1034
1057
  ];
1035
1058
 
1036
1059
  return {
@@ -1038,11 +1061,13 @@ export function registerWikiStatus(pi: ExtensionAPI): void {
1038
1061
  details: {
1039
1062
  topic: config.topic,
1040
1063
  mode: config.mode,
1041
- totalPages: Object.keys(registry.pages).length,
1042
- byType,
1064
+ knowledgeFormat: status.knowledgeFormat,
1065
+ totalPages: status.totalPages,
1066
+ byType: status.byType,
1043
1067
  orphans: orphanCount,
1044
1068
  gaps: gaps.gaps?.length || 0,
1045
1069
  health,
1070
+ blockingDiagnostics: status.blockingDiagnostics,
1046
1071
  } as Record<string, unknown>,
1047
1072
  };
1048
1073
  },
@@ -1061,11 +1086,16 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1061
1086
  parameters: Type.Object({}),
1062
1087
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1063
1088
  const paths = getPaths(ctx.cwd);
1064
- const vaultCheck = requireVault(paths);
1089
+ const vaultCheck = inspectWritableVault(paths);
1065
1090
  if (!vaultCheck.ok) {
1066
1091
  return {
1067
- content: [{ type: "text", text: vaultCheck.reason }],
1068
- 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>,
1069
1099
  isError: true,
1070
1100
  };
1071
1101
  }
@@ -1077,8 +1107,11 @@ export function registerWikiRebuildMeta(pi: ExtensionAPI, runtime?: Runtime): vo
1077
1107
  started:
1078
1108
  "\u{1F9E0} LLM Wiki: metadata rebuild started in the background — the result will be reported when it completes.",
1079
1109
  work: async () => {
1080
- rebuildMetadata(paths);
1081
- 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
+ }
1082
1115
  const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
1083
1116
  version: "1.0",
1084
1117
  last_updated: "",
@@ -1113,11 +1146,16 @@ export function registerWikiReindexEmbeddings(pi: ExtensionAPI, runtime?: Runtim
1113
1146
  }),
1114
1147
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1115
1148
  const paths = getPaths(ctx.cwd);
1116
- const vaultCheck = requireVault(paths);
1149
+ const vaultCheck = inspectWritableVault(paths);
1117
1150
  if (!vaultCheck.ok) {
1118
1151
  return {
1119
- content: [{ type: "text", text: vaultCheck.reason }],
1120
- 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>,
1121
1159
  isError: true,
1122
1160
  };
1123
1161
  }
@@ -1171,25 +1209,45 @@ export function registerWikiLogEvent(pi: ExtensionAPI): void {
1171
1209
  }),
1172
1210
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1173
1211
  const paths = getPaths(ctx.cwd);
1174
- const vaultCheck = requireVault(paths);
1212
+ const vaultCheck = inspectWritableVault(paths);
1175
1213
  if (!vaultCheck.ok) {
1176
1214
  return {
1177
- content: [{ type: "text", text: vaultCheck.reason }],
1178
- 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>,
1179
1222
  isError: true,
1180
1223
  };
1181
1224
  }
1182
1225
 
1183
- 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
+ }
1184
1242
 
1185
- // Regenerate log.md
1186
- const { buildLogMarkdown } = await import("./metadata.js");
1187
- const log = buildLogMarkdown(paths);
1188
- writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
1243
+ appendEvent(paths, { kind, ...details });
1244
+
1245
+ // Regenerate projections
1246
+ rebuildMetadata(paths);
1189
1247
 
1190
1248
  return {
1191
- content: [{ type: "text", text: `✅ Event logged: ${params.kind}` }],
1192
- details: { kind: params.kind } as Record<string, unknown>,
1249
+ content: [{ type: "text", text: `✅ Event logged: ${kind}` }],
1250
+ details: { kind } as Record<string, unknown>,
1193
1251
  };
1194
1252
  },
1195
1253
  });