@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.
- package/CHANGELOG.md +4 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +38 -3
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +347 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +174 -29
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
- package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +531 -116
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +44 -31
- package/extensions/llm-wiki/lib/tools.ts +406 -348
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +121 -130
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- package/skills/llm-wiki/SKILL.md +8 -6
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { KnowledgeDiagnostic } from "./knowledge-document.js";
|
|
4
|
+
import { type ProjectionResult, appendEvent, rebuildMetadata } from "./metadata.js";
|
|
5
|
+
import { type VaultPaths, ensureVaultStructure, fmtDate, writeJson } from "./utils.js";
|
|
6
|
+
import { inspectWritableVault, readVaultConfig } from "./vault-format.js";
|
|
7
|
+
|
|
8
|
+
export const WIKI_SCHEMA = [
|
|
9
|
+
"# LLM Wiki Schema",
|
|
10
|
+
"",
|
|
11
|
+
"## Ownership Rules",
|
|
12
|
+
"",
|
|
13
|
+
"| Path | Owner | Rule |",
|
|
14
|
+
"|------|-------|------|",
|
|
15
|
+
"| raw/** | extension | immutable after capture |",
|
|
16
|
+
"| wiki/** | model + user | editable knowledge pages |",
|
|
17
|
+
"| meta/* | extension | auto-generated |",
|
|
18
|
+
"| . | human + explicit request | operating rules |",
|
|
19
|
+
"",
|
|
20
|
+
"## Source Packet Format",
|
|
21
|
+
"",
|
|
22
|
+
"```",
|
|
23
|
+
"raw/sources/SRC-YYYY-MM-DD-NNN/",
|
|
24
|
+
" manifest.json",
|
|
25
|
+
" original/",
|
|
26
|
+
" extracted.md",
|
|
27
|
+
" attachments/",
|
|
28
|
+
"```",
|
|
29
|
+
"",
|
|
30
|
+
"## Page Types",
|
|
31
|
+
"",
|
|
32
|
+
"- **source** — what this specific source says",
|
|
33
|
+
"- **entity** — people, orgs, tools, products",
|
|
34
|
+
"- **concept** — ideas, patterns, frameworks",
|
|
35
|
+
"- **synthesis** — cross-source theses and tensions",
|
|
36
|
+
"- **analysis** — durable filed answers from queries",
|
|
37
|
+
"- **requirement** — atomic requirements with status, priority, and traceability",
|
|
38
|
+
"",
|
|
39
|
+
"## Linking Style",
|
|
40
|
+
"",
|
|
41
|
+
"- New internal links: [label](/folder/page.md)",
|
|
42
|
+
"- Legacy readable links: [[folder/page]]",
|
|
43
|
+
"- Source citation: [source](/sources/SRC-YYYY-MM-DD-NNN.md)",
|
|
44
|
+
"",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
export interface BootstrapInput {
|
|
48
|
+
topic: string;
|
|
49
|
+
mode: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type BootstrapResult =
|
|
53
|
+
| { ok: true; created: boolean; projection: ProjectionResult }
|
|
54
|
+
| { ok: false; created: false; diagnostics: KnowledgeDiagnostic[] };
|
|
55
|
+
|
|
56
|
+
export function bootstrapVault(paths: VaultPaths, input: BootstrapInput): BootstrapResult {
|
|
57
|
+
const configPath = join(paths.dotWiki, "config.json");
|
|
58
|
+
const created = !existsSync(paths.dotWiki);
|
|
59
|
+
let existing: Record<string, unknown> = {};
|
|
60
|
+
|
|
61
|
+
if (!created) {
|
|
62
|
+
const writable = inspectWritableVault(paths);
|
|
63
|
+
if (!writable.ok) return { ok: false, created: false, diagnostics: writable.diagnostics };
|
|
64
|
+
const config = readVaultConfig(paths);
|
|
65
|
+
if (!config.ok) return { ok: false, created: false, diagnostics: [config.diagnostic] };
|
|
66
|
+
existing = config.config;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const config: Record<string, unknown> = {
|
|
70
|
+
...existing,
|
|
71
|
+
name: input.topic,
|
|
72
|
+
mode: input.mode,
|
|
73
|
+
topic: input.topic,
|
|
74
|
+
created: existing.created ?? fmtDate(),
|
|
75
|
+
version: existing.version ?? "1.0",
|
|
76
|
+
...(created ? { knowledge_format: "okf-0.2" } : {}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
ensureVaultStructure(paths);
|
|
80
|
+
writeJson(configPath, config);
|
|
81
|
+
writeFileSync(join(paths.dotWiki, "WIKI_SCHEMA.md"), WIKI_SCHEMA, "utf8");
|
|
82
|
+
appendEvent(paths, { kind: "bootstrap", topic: input.topic, mode: input.mode });
|
|
83
|
+
return { ok: true, created, projection: rebuildMetadata(paths) };
|
|
84
|
+
}
|
|
@@ -3,10 +3,12 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { request as httpRequest } from "node:http";
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { parseKnowledgeDocument } from "./knowledge-document.js";
|
|
6
7
|
import type { Registry } from "./metadata.js";
|
|
7
8
|
import type { LaunchCtx, Runtime } from "./runtime.js";
|
|
8
9
|
import type { TaskConfig } from "./task-config.js";
|
|
9
|
-
import { type VaultPaths,
|
|
10
|
+
import { type VaultPaths, readJson, writeJson } from "./utils.js";
|
|
11
|
+
import { assertWritableVault } from "./vault-format.js";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Background semantic embeddings, computed at write time (issue #66, epic #63).
|
|
@@ -166,6 +168,7 @@ export function readEmbeddingStore(paths: VaultPaths): EmbeddingStore {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
export function writeEmbeddingStore(paths: VaultPaths, store: EmbeddingStore): void {
|
|
171
|
+
assertWritableVault(paths);
|
|
169
172
|
writeJson(embeddingStorePath(paths), store);
|
|
170
173
|
}
|
|
171
174
|
|
|
@@ -189,8 +192,9 @@ function readPageText(paths: VaultPaths, id: string): PageText | undefined {
|
|
|
189
192
|
const pagePath = join(paths.wiki, `${id}.md`);
|
|
190
193
|
if (!existsSync(pagePath)) return undefined;
|
|
191
194
|
const raw = readFileSync(pagePath, "utf-8");
|
|
192
|
-
const
|
|
193
|
-
|
|
195
|
+
const result = parseKnowledgeDocument(raw, `${id}.md`);
|
|
196
|
+
if (!result.ok) return undefined;
|
|
197
|
+
const text = buildEmbeddingText(id, result.document.frontmatter, result.document.body);
|
|
194
198
|
return { id, text, hash: contentHash(text) };
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -205,6 +209,7 @@ export async function embedPages(
|
|
|
205
209
|
embedder: Embedder,
|
|
206
210
|
opts: { force?: boolean } = {},
|
|
207
211
|
): Promise<EmbedStats> {
|
|
212
|
+
assertWritableVault(paths);
|
|
208
213
|
const store = readEmbeddingStore(paths);
|
|
209
214
|
const targets: PageText[] = [];
|
|
210
215
|
let skipped = 0;
|
|
@@ -251,6 +256,7 @@ export async function reindexEmbeddings(
|
|
|
251
256
|
embedder: Embedder,
|
|
252
257
|
opts: { force?: boolean } = {},
|
|
253
258
|
): Promise<ReindexStats> {
|
|
259
|
+
assertWritableVault(paths);
|
|
254
260
|
const registry = readJson<Registry>(join(paths.meta, "registry.json"), {
|
|
255
261
|
version: "1.0",
|
|
256
262
|
last_updated: "",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { resolve, sep } from "node:path";
|
|
2
1
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
3
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
3
|
import { scheduleReindex } from "./indexing.js";
|
|
5
4
|
import { rebuildMetadataLight } from "./metadata.js";
|
|
6
5
|
import type { Runtime } from "./runtime.js";
|
|
7
|
-
import { isProtectedPath, resolveVaultPaths } from "./utils.js";
|
|
6
|
+
import { isPathWithin, isProtectedPath, resolveVaultPaths } from "./utils.js";
|
|
7
|
+
import type { VaultPaths } from "./utils.js";
|
|
8
|
+
import { inspectVaultFormat, isGeneratedOkfPath } from "./vault-format.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Guardrails and auto-rebuild hooks for the LLM Wiki extension.
|
|
@@ -12,64 +13,208 @@ import { isProtectedPath, resolveVaultPaths } from "./utils.js";
|
|
|
12
13
|
|
|
13
14
|
let pendingRebuild = false;
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
+
const APPLY_PATCH_PATH_NOISE =
|
|
17
|
+
/^\*{0,3}\s*(?:(?:update|add|delete|move)[^A-Za-z0-9]*(?:file|to)?[^A-Za-z0-9]*:)?\s*\*{0,3}\s*/i;
|
|
18
|
+
const PATCH_INPUT_KEYS: Record<string, true> = { input: true, _input: true, patch: true };
|
|
19
|
+
const DESTINATION_KEYS: Record<string, true> = {
|
|
20
|
+
rename: true,
|
|
21
|
+
move: true,
|
|
22
|
+
dest: true,
|
|
23
|
+
destination: true,
|
|
24
|
+
newPath: true,
|
|
25
|
+
};
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
interface MutationScan {
|
|
28
|
+
paths: string[];
|
|
29
|
+
complete: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeMutationPath(target: string): string | undefined {
|
|
33
|
+
const trimmed = target.trim();
|
|
34
|
+
if (!trimmed) return undefined;
|
|
35
|
+
const first = trimmed[0];
|
|
36
|
+
const last = trimmed[trimmed.length - 1];
|
|
37
|
+
const quoted = first === '"' || first === "'";
|
|
38
|
+
if (quoted !== (last === '"' || last === "'") || (quoted && first !== last)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const unquoted = quoted ? trimmed.slice(1, -1) : trimmed;
|
|
42
|
+
return unquoted.replace(APPLY_PATCH_PATH_NOISE, "") || undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parsePatchHeader(line: string): string | undefined {
|
|
46
|
+
const trimmed = line.replace(/\r$/, "").trimEnd();
|
|
47
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return undefined;
|
|
48
|
+
|
|
49
|
+
const body = trimmed.slice(1, -1).trim();
|
|
50
|
+
const tag = /#[0-9A-Fa-f]{4}\s*$/.exec(body);
|
|
51
|
+
const rawTarget = tag ? body.slice(0, tag.index) : body.replace(/\s+$/, "");
|
|
52
|
+
if (!rawTarget || rawTarget.includes("#")) return undefined;
|
|
53
|
+
|
|
54
|
+
return normalizeMutationPath(rawTarget);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseMoveDestination(line: string): string | undefined {
|
|
58
|
+
const rawDestination = line.trim().slice(2).trim();
|
|
59
|
+
if (!rawDestination) return undefined;
|
|
60
|
+
const quote = rawDestination[0];
|
|
61
|
+
if (quote !== '"' && quote !== "'") return normalizeMutationPath(rawDestination);
|
|
62
|
+
|
|
63
|
+
let cursor = 1;
|
|
64
|
+
while (cursor < rawDestination.length) {
|
|
65
|
+
if (rawDestination[cursor] === "\\" && cursor + 1 < rawDestination.length) {
|
|
66
|
+
cursor += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (rawDestination[cursor] === quote) {
|
|
70
|
+
return cursor === rawDestination.length - 1
|
|
71
|
+
? normalizeMutationPath(rawDestination)
|
|
72
|
+
: undefined;
|
|
73
|
+
}
|
|
74
|
+
cursor++;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scanPatchString(input: string): MutationScan {
|
|
80
|
+
const paths: string[] = [];
|
|
81
|
+
let sawHeader = false;
|
|
82
|
+
let sectionHasMove = false;
|
|
83
|
+
let complete = true;
|
|
84
|
+
const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
|
|
85
|
+
|
|
86
|
+
for (const line of stripped.split("\n")) {
|
|
87
|
+
const trimmed = line.replace(/\r$/, "").trim();
|
|
88
|
+
if (trimmed.startsWith("[")) {
|
|
89
|
+
sawHeader = true;
|
|
90
|
+
sectionHasMove = false;
|
|
91
|
+
const path = parsePatchHeader(line);
|
|
92
|
+
if (path) paths.push(path);
|
|
93
|
+
else complete = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!/^MV(?:\s|$)/.test(trimmed)) continue;
|
|
97
|
+
const destination = parseMoveDestination(trimmed);
|
|
98
|
+
if (!sawHeader || sectionHasMove || !destination) complete = false;
|
|
99
|
+
else {
|
|
100
|
+
paths.push(destination);
|
|
101
|
+
sectionHasMove = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { paths, complete: sawHeader && complete };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mergeMutationScans(target: MutationScan, source: MutationScan): void {
|
|
109
|
+
target.paths.push(...source.paths);
|
|
110
|
+
target.complete &&= source.complete;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function addMutationPath(scan: MutationScan, target: string): void {
|
|
114
|
+
const path = normalizeMutationPath(target);
|
|
115
|
+
if (path) scan.paths.push(path);
|
|
116
|
+
else scan.complete = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectMutationPaths(
|
|
120
|
+
input: unknown,
|
|
121
|
+
seen: WeakSet<object>,
|
|
122
|
+
stringsArePatches = false,
|
|
123
|
+
): MutationScan {
|
|
18
124
|
if (typeof input === "string") {
|
|
19
|
-
return
|
|
125
|
+
return stringsArePatches ? scanPatchString(input) : { paths: [], complete: true };
|
|
126
|
+
}
|
|
127
|
+
if (!input || typeof input !== "object" || seen.has(input)) {
|
|
128
|
+
return { paths: [], complete: true };
|
|
20
129
|
}
|
|
21
|
-
if (!input || typeof input !== "object" || seen.has(input)) return [];
|
|
22
130
|
|
|
23
131
|
seen.add(input);
|
|
24
|
-
|
|
132
|
+
const scan: MutationScan = { paths: [], complete: true };
|
|
133
|
+
if (Array.isArray(input)) {
|
|
134
|
+
for (const value of input) {
|
|
135
|
+
mergeMutationScans(scan, collectMutationPaths(value, seen, stringsArePatches));
|
|
136
|
+
}
|
|
137
|
+
return scan;
|
|
138
|
+
}
|
|
25
139
|
|
|
26
|
-
const
|
|
27
|
-
if (typeof path === "string" && path.length > 0)
|
|
140
|
+
const record = input as Record<string, unknown>;
|
|
141
|
+
if (typeof record.path === "string" && record.path.length > 0) {
|
|
142
|
+
addMutationPath(scan, record.path);
|
|
143
|
+
}
|
|
144
|
+
const eventPaths = Array.isArray(record.paths) ? record.paths : [record.paths];
|
|
145
|
+
for (const path of eventPaths) {
|
|
146
|
+
if (typeof path === "string" && path.length > 0) addMutationPath(scan, path);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const [key, value] of Object.entries(record)) {
|
|
150
|
+
if (key === "path" || key === "paths") continue;
|
|
151
|
+
if (DESTINATION_KEYS[key] === true) {
|
|
152
|
+
if (typeof value === "string" && value.length > 0) addMutationPath(scan, value);
|
|
153
|
+
else scan.complete = false;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const childStringsArePatches = stringsArePatches || PATCH_INPUT_KEYS[key] === true;
|
|
157
|
+
if (typeof value !== "string" && (!value || typeof value !== "object")) continue;
|
|
158
|
+
mergeMutationScans(scan, collectMutationPaths(value, seen, childStringsArePatches));
|
|
159
|
+
}
|
|
160
|
+
return scan;
|
|
161
|
+
}
|
|
28
162
|
|
|
29
|
-
|
|
163
|
+
function inspectMutationPaths(input: unknown): MutationScan {
|
|
164
|
+
const stringsArePatches = typeof input === "string" || Array.isArray(input);
|
|
165
|
+
const scan = collectMutationPaths(input, new WeakSet(), stringsArePatches);
|
|
166
|
+
return { paths: [...new Set(scan.paths)], complete: scan.complete };
|
|
30
167
|
}
|
|
31
168
|
|
|
32
169
|
/** Return every file path targeted by a write or patch-shaped edit input. */
|
|
33
170
|
export function extractMutationPaths(input: unknown): string[] {
|
|
34
|
-
return
|
|
171
|
+
return inspectMutationPaths(input).paths;
|
|
35
172
|
}
|
|
36
173
|
|
|
37
174
|
/** True when a write or patch-shaped edit targets a page in the wiki directory. */
|
|
38
175
|
export function hasWikiMutation(input: unknown, wikiPath: string): boolean {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
176
|
+
return extractMutationPaths(input).some((path) => isPathWithin(wikiPath, path));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function mutationBlockReason(path: string, paths: VaultPaths): string | undefined {
|
|
180
|
+
const protectedPath = isProtectedPath(path, paths);
|
|
181
|
+
if (protectedPath.protected) return protectedPath.reason;
|
|
182
|
+
|
|
183
|
+
if (isPathWithin(paths.dotWiki, path)) {
|
|
184
|
+
const state = inspectVaultFormat(paths);
|
|
185
|
+
if (state.blocking) {
|
|
186
|
+
return `Wiki vault configuration is invalid: ${state.diagnostics[0].message}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isGeneratedOkfPath(path, paths)) {
|
|
191
|
+
return "Generated OKF indexes and log are read-only. Use wiki_rebuild_meta or the page-producing tool that owns the source mutation.";
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
46
194
|
}
|
|
47
195
|
|
|
48
196
|
/** Install guardrails on the extension API. */
|
|
49
197
|
export function installGuardrails(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
50
|
-
// Block direct edits to raw/ and meta
|
|
198
|
+
// Block direct edits to raw/ and meta/, plus OKF generated projections
|
|
51
199
|
pi.on("tool_call", async (event) => {
|
|
52
200
|
if (isToolCallEventType("write", event)) {
|
|
53
201
|
const path = event.input.path as string;
|
|
54
202
|
const paths = resolveVaultPaths(process.cwd());
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
return { block: true, reason: check.reason };
|
|
58
|
-
}
|
|
203
|
+
const reason = mutationBlockReason(path, paths);
|
|
204
|
+
if (reason) return { block: true, reason };
|
|
59
205
|
}
|
|
60
206
|
|
|
61
207
|
if (isToolCallEventType("edit", event)) {
|
|
62
|
-
const
|
|
63
|
-
|
|
208
|
+
const mutation = inspectMutationPaths(event.input);
|
|
209
|
+
const targetPaths = mutation.paths;
|
|
210
|
+
if (!mutation.complete || targetPaths.length === 0) {
|
|
64
211
|
return { block: true, reason: "Cannot determine the files targeted by this edit." };
|
|
65
212
|
}
|
|
66
213
|
|
|
67
214
|
const paths = resolveVaultPaths(process.cwd());
|
|
68
215
|
for (const path of targetPaths) {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
return { block: true, reason: check.reason };
|
|
72
|
-
}
|
|
216
|
+
const reason = mutationBlockReason(path, paths);
|
|
217
|
+
if (reason) return { block: true, reason };
|
|
73
218
|
}
|
|
74
219
|
}
|
|
75
220
|
});
|
|
@@ -64,7 +64,8 @@ export function scheduleReindex(
|
|
|
64
64
|
// that land during embedding are not lost.
|
|
65
65
|
while (dirty.has(root)) {
|
|
66
66
|
dirty.delete(root);
|
|
67
|
-
rebuildMetadataLight(paths);
|
|
67
|
+
const projection = rebuildMetadataLight(paths);
|
|
68
|
+
if (!projection.ok) continue;
|
|
68
69
|
|
|
69
70
|
// Refresh embeddings only after metadata is consistent. Stale-aware and
|
|
70
71
|
// a no-op unless an embedder is configured.
|
|
@@ -4,9 +4,19 @@ import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
|
4
4
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import type { Static } from "typebox";
|
|
7
|
+
import {
|
|
8
|
+
type KnowledgeDiagnostic,
|
|
9
|
+
type KnowledgeDocument,
|
|
10
|
+
createKnowledgeDocument,
|
|
11
|
+
patchKnowledgeDocument,
|
|
12
|
+
readKnowledgeDocumentFile,
|
|
13
|
+
serializeKnowledgeDocument,
|
|
14
|
+
writeKnowledgeDocumentFile,
|
|
15
|
+
} from "./knowledge-document.js";
|
|
7
16
|
import { appendEvent, rebuildMetadataLight } from "./metadata.js";
|
|
8
17
|
import { runSubAgent } from "./subagent.js";
|
|
9
18
|
import { type VaultPaths, fmtDate, slugify } from "./utils.js";
|
|
19
|
+
import { VaultWriteError, assertWritableVault } from "./vault-format.js";
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Background ingest synthesis (issue #65, part of epic #63).
|
|
@@ -76,39 +86,51 @@ export interface CommitResult {
|
|
|
76
86
|
contradictions: number;
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
export type CommitSynthesisOutcome =
|
|
90
|
+
| ({ ok: true } & CommitResult)
|
|
91
|
+
| { ok: false; sourceId: string; diagnostics: KnowledgeDiagnostic[] };
|
|
92
|
+
|
|
79
93
|
// ── deterministic persistence (no LLM) ────────────────────
|
|
80
94
|
|
|
81
|
-
function
|
|
82
|
-
title: string,
|
|
83
|
-
description: string,
|
|
84
|
-
date: string,
|
|
85
|
-
sourceId: string,
|
|
86
|
-
): string {
|
|
95
|
+
function buildEntityPageBody(title: string, description: string, sourceId: string): string {
|
|
87
96
|
const desc = description.trim() || "One-line description.";
|
|
88
|
-
return
|
|
97
|
+
return `# ${title}
|
|
98
|
+
|
|
99
|
+
${desc}
|
|
100
|
+
|
|
101
|
+
## Overview
|
|
102
|
+
|
|
103
|
+
[Key facts]
|
|
104
|
+
|
|
105
|
+
## Links
|
|
106
|
+
|
|
107
|
+
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
function
|
|
92
|
-
title: string,
|
|
93
|
-
definition: string,
|
|
94
|
-
date: string,
|
|
95
|
-
sourceId: string,
|
|
96
|
-
): string {
|
|
110
|
+
function buildConceptPageBody(title: string, definition: string, sourceId: string): string {
|
|
97
111
|
const def = definition.trim() || "One-line definition.";
|
|
98
|
-
return
|
|
112
|
+
return `# ${title}
|
|
113
|
+
|
|
114
|
+
${def}
|
|
115
|
+
|
|
116
|
+
## Definition
|
|
117
|
+
|
|
118
|
+
[Clear explanation]
|
|
119
|
+
|
|
120
|
+
## Links
|
|
121
|
+
|
|
122
|
+
- [${sourceId}](/sources/${sourceId}.md)`;
|
|
99
123
|
}
|
|
100
124
|
|
|
101
|
-
/** Rebuild the source page from synthesis data, marking it ingested. */
|
|
102
|
-
export function
|
|
125
|
+
/** Rebuild the source page body from synthesis data, marking it ingested. */
|
|
126
|
+
export function buildIngestedSourcePageBody(
|
|
103
127
|
manifest: Record<string, unknown>,
|
|
104
128
|
data: SynthesisData,
|
|
105
|
-
|
|
129
|
+
_date: string,
|
|
106
130
|
): string {
|
|
107
131
|
const id = String(manifest.id);
|
|
108
132
|
const title = String(manifest.title || id);
|
|
109
133
|
const url = manifest.url ? `\n> _Original: [${manifest.url}](${manifest.url})_` : "";
|
|
110
|
-
const format = String(manifest.format || "unknown");
|
|
111
|
-
const captured = String(manifest.captured || date);
|
|
112
134
|
|
|
113
135
|
const takeaways =
|
|
114
136
|
data.key_takeaways.length > 0
|
|
@@ -116,11 +138,11 @@ export function buildIngestedSourcePage(
|
|
|
116
138
|
: "- [None recorded]";
|
|
117
139
|
const entities =
|
|
118
140
|
data.entities.length > 0
|
|
119
|
-
? data.entities.map((e) => `- [
|
|
141
|
+
? data.entities.map((e) => `- [${e.title}](/entities/${slugify(e.title)}.md)`).join("\n")
|
|
120
142
|
: "- [None]";
|
|
121
143
|
const concepts =
|
|
122
144
|
data.concepts.length > 0
|
|
123
|
-
? data.concepts.map((c) => `- [
|
|
145
|
+
? data.concepts.map((c) => `- [${c.title}](/concepts/${slugify(c.title)}.md)`).join("\n")
|
|
124
146
|
: "- [None]";
|
|
125
147
|
const quotes =
|
|
126
148
|
data.quotes && data.quotes.length > 0
|
|
@@ -133,7 +155,61 @@ export function buildIngestedSourcePage(
|
|
|
133
155
|
? `\n## Contradictions\n\n${data.contradictions.map((c) => `⚠️ **Contradiction**: ${c.trim()}`).join("\n")}\n`
|
|
134
156
|
: "";
|
|
135
157
|
|
|
136
|
-
return
|
|
158
|
+
return `# ${title}${url}
|
|
159
|
+
|
|
160
|
+
## Summary
|
|
161
|
+
|
|
162
|
+
${data.summary.trim()}
|
|
163
|
+
|
|
164
|
+
## Key Takeaways
|
|
165
|
+
|
|
166
|
+
${takeaways}
|
|
167
|
+
|
|
168
|
+
## Entities Mentioned
|
|
169
|
+
|
|
170
|
+
${entities}
|
|
171
|
+
|
|
172
|
+
## Concepts Mentioned
|
|
173
|
+
|
|
174
|
+
${concepts}
|
|
175
|
+
|
|
176
|
+
## Notable Quotes
|
|
177
|
+
|
|
178
|
+
${quotes}
|
|
179
|
+
${contradictions}## Source Packet
|
|
180
|
+
|
|
181
|
+
- **ID:** \`sources/${id}\`
|
|
182
|
+
- **Extracted:** \`raw/sources/${id}/extracted.md\`
|
|
183
|
+
- **Manifest:** \`raw/sources/${id}/manifest.json\`
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Rebuild the source page from synthesis data, marking it ingested. */
|
|
188
|
+
export function buildIngestedSourcePage(
|
|
189
|
+
manifest: Record<string, unknown>,
|
|
190
|
+
data: SynthesisData,
|
|
191
|
+
date: string,
|
|
192
|
+
): string {
|
|
193
|
+
const id = String(manifest.id);
|
|
194
|
+
const title = String(manifest.title || id);
|
|
195
|
+
const format = String(manifest.format || "unknown");
|
|
196
|
+
const captured = String(manifest.captured || date);
|
|
197
|
+
const body = buildIngestedSourcePageBody(manifest, data, date);
|
|
198
|
+
const doc = createKnowledgeDocument(
|
|
199
|
+
`sources/${id}.md`,
|
|
200
|
+
{
|
|
201
|
+
type: "source",
|
|
202
|
+
title,
|
|
203
|
+
format,
|
|
204
|
+
source_id: id,
|
|
205
|
+
raw_path: `raw/sources/${id}/extracted.md`,
|
|
206
|
+
captured,
|
|
207
|
+
status: "ingested",
|
|
208
|
+
updated: date,
|
|
209
|
+
},
|
|
210
|
+
body,
|
|
211
|
+
);
|
|
212
|
+
return serializeKnowledgeDocument(doc);
|
|
137
213
|
}
|
|
138
214
|
|
|
139
215
|
/**
|
|
@@ -147,7 +223,7 @@ export function commitSynthesis(
|
|
|
147
223
|
manifest: Record<string, unknown>,
|
|
148
224
|
data: SynthesisData,
|
|
149
225
|
date: string = fmtDate(),
|
|
150
|
-
):
|
|
226
|
+
): CommitSynthesisOutcome {
|
|
151
227
|
const result: CommitResult = {
|
|
152
228
|
sourceId,
|
|
153
229
|
sourcePage: join(paths.wiki, "sources", `${sourceId}.md`),
|
|
@@ -158,9 +234,42 @@ export function commitSynthesis(
|
|
|
158
234
|
contradictions: data.contradictions?.length ?? 0,
|
|
159
235
|
};
|
|
160
236
|
|
|
161
|
-
|
|
237
|
+
try {
|
|
238
|
+
assertWritableVault(paths);
|
|
239
|
+
} catch (error: unknown) {
|
|
240
|
+
if (error instanceof VaultWriteError) {
|
|
241
|
+
return { ok: false, sourceId, diagnostics: error.diagnostics };
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Patch existing documents so unknown fields, legacy sources, and titles survive.
|
|
247
|
+
let sourceDocument: KnowledgeDocument;
|
|
248
|
+
if (existsSync(result.sourcePage)) {
|
|
249
|
+
const parsed = readKnowledgeDocumentFile(result.sourcePage, `sources/${sourceId}.md`);
|
|
250
|
+
if (!parsed.ok) return { ok: false, sourceId, diagnostics: parsed.diagnostics };
|
|
251
|
+
sourceDocument = patchKnowledgeDocument(parsed.document, {
|
|
252
|
+
fields: { status: "ingested", updated: date },
|
|
253
|
+
body: buildIngestedSourcePageBody(manifest, data, date),
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
sourceDocument = createKnowledgeDocument(
|
|
257
|
+
`sources/${sourceId}.md`,
|
|
258
|
+
{
|
|
259
|
+
type: "source",
|
|
260
|
+
title: String(manifest.title || sourceId),
|
|
261
|
+
format: String(manifest.format || "unknown"),
|
|
262
|
+
source_id: sourceId,
|
|
263
|
+
raw_path: `raw/sources/${sourceId}/extracted.md`,
|
|
264
|
+
captured: String(manifest.captured || date),
|
|
265
|
+
status: "ingested",
|
|
266
|
+
updated: date,
|
|
267
|
+
},
|
|
268
|
+
buildIngestedSourcePageBody(manifest, data, date),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
162
271
|
mkdirSync(join(paths.wiki, "sources"), { recursive: true });
|
|
163
|
-
|
|
272
|
+
writeKnowledgeDocumentFile(result.sourcePage, sourceDocument);
|
|
164
273
|
|
|
165
274
|
// Entity pages — create if absent, link if present.
|
|
166
275
|
mkdirSync(join(paths.wiki, "entities"), { recursive: true });
|
|
@@ -171,7 +280,19 @@ export function commitSynthesis(
|
|
|
171
280
|
if (existsSync(pagePath)) {
|
|
172
281
|
result.entitiesLinked.push(slug);
|
|
173
282
|
} else {
|
|
174
|
-
|
|
283
|
+
const entityDoc = createKnowledgeDocument(
|
|
284
|
+
`entities/${slug}.md`,
|
|
285
|
+
{
|
|
286
|
+
type: "entity",
|
|
287
|
+
title: e.title,
|
|
288
|
+
description: e.description.trim() || "One-line description.",
|
|
289
|
+
created: date,
|
|
290
|
+
updated: date,
|
|
291
|
+
},
|
|
292
|
+
buildEntityPageBody(e.title, e.description, sourceId),
|
|
293
|
+
[{ id: sourceId, resource: `/sources/${sourceId}.md` }],
|
|
294
|
+
);
|
|
295
|
+
writeKnowledgeDocumentFile(pagePath, entityDoc);
|
|
175
296
|
result.entitiesCreated.push(slug);
|
|
176
297
|
}
|
|
177
298
|
}
|
|
@@ -185,7 +306,19 @@ export function commitSynthesis(
|
|
|
185
306
|
if (existsSync(pagePath)) {
|
|
186
307
|
result.conceptsLinked.push(slug);
|
|
187
308
|
} else {
|
|
188
|
-
|
|
309
|
+
const conceptDoc = createKnowledgeDocument(
|
|
310
|
+
`concepts/${slug}.md`,
|
|
311
|
+
{
|
|
312
|
+
type: "concept",
|
|
313
|
+
title: c.title,
|
|
314
|
+
description: c.definition.trim() || "One-line definition.",
|
|
315
|
+
created: date,
|
|
316
|
+
updated: date,
|
|
317
|
+
},
|
|
318
|
+
buildConceptPageBody(c.title, c.definition, sourceId),
|
|
319
|
+
[{ id: sourceId, resource: `/sources/${sourceId}.md` }],
|
|
320
|
+
);
|
|
321
|
+
writeKnowledgeDocumentFile(pagePath, conceptDoc);
|
|
189
322
|
result.conceptsCreated.push(slug);
|
|
190
323
|
}
|
|
191
324
|
}
|
|
@@ -199,7 +332,7 @@ export function commitSynthesis(
|
|
|
199
332
|
background: true,
|
|
200
333
|
});
|
|
201
334
|
|
|
202
|
-
return result;
|
|
335
|
+
return { ok: true, ...result };
|
|
203
336
|
}
|
|
204
337
|
|
|
205
338
|
// ── sub-agent synthesis (LLM) ─────────────────────────────
|
|
@@ -253,7 +386,15 @@ export async function runIngestSynthesis(
|
|
|
253
386
|
"Persist the structured synthesis of this source into wiki pages. Call exactly once.",
|
|
254
387
|
parameters: CommitSynthesisSchema,
|
|
255
388
|
execute: async (_id, params) => {
|
|
256
|
-
|
|
389
|
+
const outcome = commitSynthesis(paths, sourceId, manifest, params);
|
|
390
|
+
if (!outcome.ok) {
|
|
391
|
+
return {
|
|
392
|
+
content: [{ type: "text", text: `Failed: ${outcome.diagnostics[0].message}` }],
|
|
393
|
+
details: { sourceId },
|
|
394
|
+
isError: true,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
committed = outcome;
|
|
257
398
|
const ack = `Committed: source page + ${committed.entitiesCreated.length} new entit${
|
|
258
399
|
committed.entitiesCreated.length === 1 ? "y" : "ies"
|
|
259
400
|
}, ${committed.conceptsCreated.length} new concept${
|