@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
@@ -0,0 +1,1174 @@
1
+ # OKF Foundation Release Remediation Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use /skill:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Remove the verified OKF Foundation release blockers by making the legacy layout migration safe and publishable, eliminating first-party unresolved source-page links, stabilizing the MCP output-cap test, and raising a reviewed PR.
6
+
7
+ **Architecture:** Keep this as a narrow Foundation release-remediation change. The existing layout migration remains a standalone Node script, but it will resolve paths consistently, reject destination conflicts before mutation, move files and directories through one rollback-capable executor, and ship in the npm tarball. Source pages will represent extension-owned raw artifacts as code paths rather than OKF knowledge links, while valid entity/concept links remain Markdown links.
8
+
9
+ **Tech Stack:** Node.js filesystem/path APIs, TypeScript ES2022, Vitest, npm package manifests, Biome, GitHub CLI
10
+
11
+ **Roadmap:** `docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md`
12
+
13
+ **Phase:** Phase 1 remediation: OKF Foundation release gate
14
+
15
+ ---
16
+
17
+ ## Scope and release boundary
18
+
19
+ This plan fixes only findings reproduced during the Foundation release gate:
20
+
21
+ 1. `scripts/migrate-llm-wiki.js` mishandles absolute legacy-vault paths.
22
+ 2. Its legacy apply path creates `.llm-wiki/config.json` as a directory and then fails with `EISDIR`.
23
+ 3. Existing destination entries can cause a split vault because the script skips them instead of aborting before mutation.
24
+ 4. The advertised migration script is omitted from the npm package.
25
+ 5. Generated source pages emit invalid raw-artifact Markdown links and unresolved skeleton placeholder links.
26
+ 6. The MCP 16 MiB output-cap test uses a load-sensitive five-second deadline.
27
+
28
+ This phase intentionally does **not** add `wiki_okf_migrate`, OKF content conversion, import, export, review staging, or transaction journals. Existing old-layout vaults already remain readable and writable without migration; this plan repairs the separately advertised optional layout migration. No package version is edited manually.
29
+
30
+ ## File responsibility map
31
+
32
+ ### New files
33
+
34
+ - `test/migration-script.test.ts` — black-box CLI coverage for dry-run, absolute/relative paths, paths with spaces, successful apply, byte preservation, conflict preflight, idempotency, doubled-layout recovery, and npm-package inclusion.
35
+
36
+ ### Modified files
37
+
38
+ - `scripts/migrate-llm-wiki.js` — common positional-path resolution, correct destination-parent creation, conflict preflight, rollback of synchronous move failures, and clearer failure output.
39
+ - `package.json` — include only the user-facing migration script in published files.
40
+ - `.github/workflows/ci.yml` — execute the packed migration script with the minimum supported Node 18 runtime.
41
+ - `extensions/llm-wiki/lib/source-packet.ts` — emit plain placeholders and code-form raw packet paths in skeleton pages.
42
+ - `extensions/llm-wiki/lib/ingest-worker.ts` — emit code-form raw packet paths in ingested pages while retaining valid entity/concept Markdown links.
43
+ - `test/source-capture.test.ts` — assert captured skeleton pages create no first-party unresolved-link diagnostics.
44
+ - `test/ingest-worker.test.ts` — assert ingested source/entity/concept output creates no first-party unresolved-link diagnostics.
45
+ - `test/mcp-exec.test.ts` — give the real 16 MiB stress case enough time under parallel coverage without weakening its byte and UTF-8 assertions.
46
+ - `CHANGELOG.md` — record the migration safety/packaging and generated-link fixes under `Unreleased`.
47
+
48
+ No README localization changes are needed because no README currently documents this script or generated source-page rendering.
49
+
50
+ ---
51
+
52
+ ### Task 1: Make the legacy layout migration correct and fail before known conflicts
53
+
54
+ **Files:**
55
+ - Create: `test/migration-script.test.ts`
56
+ - Modify: `scripts/migrate-llm-wiki.js:28-57`
57
+ - Modify: `scripts/migrate-llm-wiki.js:62-69`
58
+ - Modify: `scripts/migrate-llm-wiki.js:151-310`
59
+
60
+ - [ ] **Step 1: Create a branch from current `main`**
61
+
62
+ Run:
63
+
64
+ ```bash
65
+ git fetch origin
66
+ git switch -c fix/okf-foundation-release-gate origin/main
67
+ git add docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md
68
+ git commit -m "docs: plan OKF Foundation release remediation"
69
+ ```
70
+
71
+ Expected: branch `fix/okf-foundation-release-gate` starts at current `origin/main`, the reviewed plan is its first commit, and `git status --short` is empty.
72
+
73
+ - [ ] **Step 2: Write black-box migration regression tests**
74
+
75
+ Create `test/migration-script.test.ts`:
76
+
77
+ ```ts
78
+ import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from "node:child_process";
79
+ import { createHash } from "node:crypto";
80
+ import {
81
+ existsSync,
82
+ mkdirSync,
83
+ mkdtempSync,
84
+ readFileSync,
85
+ readdirSync,
86
+ rmSync,
87
+ statSync,
88
+ writeFileSync,
89
+ } from "node:fs";
90
+ import { tmpdir } from "node:os";
91
+ import { basename, dirname, join, relative } from "node:path";
92
+ import { afterEach, describe, expect, it } from "vitest";
93
+ import { rootDir } from "./helpers.js";
94
+
95
+ const script = join(rootDir, "scripts", "migrate-llm-wiki.js");
96
+ const roots: string[] = [];
97
+ const migratedPaths: Record<string, string> = {
98
+ ".wiki/config.json": ".llm-wiki/config.json",
99
+ ".wiki/templates/concept.md": ".llm-wiki/templates/concept.md",
100
+ "raw/sources/SRC-OLD/extracted.md": ".llm-wiki/raw/sources/SRC-OLD/extracted.md",
101
+ "raw/sources/SRC-OLD/original/input.txt":
102
+ ".llm-wiki/raw/sources/SRC-OLD/original/input.txt",
103
+ "wiki/concepts/legacy.md": ".llm-wiki/wiki/concepts/legacy.md",
104
+ "meta/registry.json": ".llm-wiki/meta/registry.json",
105
+ "outputs/report.md": ".llm-wiki/outputs/report.md",
106
+ ".discoveries/state.json": ".llm-wiki/.discoveries/state.json",
107
+ "WIKI_SCHEMA.md": ".llm-wiki/WIKI_SCHEMA.md",
108
+ };
109
+
110
+ afterEach(() => {
111
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
112
+ });
113
+
114
+ function tempRoot(prefix = "pi llm wiki migration "): string {
115
+ const root = mkdtempSync(join(tmpdir(), prefix));
116
+ roots.push(root);
117
+ return root;
118
+ }
119
+
120
+ function runMigration(
121
+ args: string[],
122
+ cwd = rootDir,
123
+ ): { status: number | null; stdout: string; stderr: string } {
124
+ const result = spawnSync(process.execPath, [script, ...args], {
125
+ cwd,
126
+ encoding: "utf8",
127
+ });
128
+ return { status: result.status, stdout: result.stdout, stderr: result.stderr };
129
+ }
130
+
131
+ function seedLegacy(root: string): void {
132
+ mkdirSync(join(root, ".wiki", "templates"), { recursive: true });
133
+ mkdirSync(join(root, "raw", "sources", "SRC-OLD", "original"), { recursive: true });
134
+ mkdirSync(join(root, "wiki", "concepts"), { recursive: true });
135
+ mkdirSync(join(root, "meta"), { recursive: true });
136
+ mkdirSync(join(root, "outputs"), { recursive: true });
137
+ mkdirSync(join(root, ".discoveries"), { recursive: true });
138
+ writeFileSync(join(root, ".wiki", "config.json"), '{"name":"Legacy"}\n');
139
+ writeFileSync(join(root, ".wiki", "templates", "concept.md"), "template bytes\n");
140
+ writeFileSync(join(root, ".wiki", "extra.txt"), "leave in old marker directory\n");
141
+ writeFileSync(join(root, "raw", "sources", "SRC-OLD", "extracted.md"), "raw bytes\n");
142
+ writeFileSync(
143
+ join(root, "raw", "sources", "SRC-OLD", "original", "input.txt"),
144
+ "original bytes\n",
145
+ );
146
+ writeFileSync(
147
+ join(root, "wiki", "concepts", "legacy.md"),
148
+ "---\ntype: concept\nsources: sources/SRC-OLD\nunknown: keep\n---\n\nLegacy body.\n",
149
+ );
150
+ writeFileSync(join(root, "meta", "registry.json"), '{"pages":{}}\n');
151
+ writeFileSync(join(root, "outputs", "report.md"), "report bytes\n");
152
+ writeFileSync(join(root, ".discoveries", "state.json"), "discovery bytes\n");
153
+ writeFileSync(join(root, "WIKI_SCHEMA.md"), "schema bytes\n");
154
+ }
155
+
156
+ function snapshot(root: string, directory = root): Record<string, string> {
157
+ const files: Record<string, string> = {};
158
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
159
+ a.name.localeCompare(b.name),
160
+ )) {
161
+ const path = join(directory, entry.name);
162
+ if (entry.isDirectory()) Object.assign(files, snapshot(root, path));
163
+ else if (entry.isFile()) {
164
+ files[relative(root, path).replace(/\\/g, "/")] = createHash("sha256")
165
+ .update(readFileSync(path))
166
+ .digest("hex");
167
+ }
168
+ }
169
+ return files;
170
+ }
171
+
172
+ function assertMigrated(root: string, before: Record<string, string>): void {
173
+ expect(statSync(join(root, ".llm-wiki", "config.json")).isFile()).toBe(true);
174
+ expect(readFileSync(join(root, ".llm-wiki", "config.json"), "utf8")).toBe(
175
+ '{"name":"Legacy"}\n',
176
+ );
177
+ expect(readFileSync(join(root, ".llm-wiki", "templates", "concept.md"), "utf8")).toBe(
178
+ "template bytes\n",
179
+ );
180
+ expect(
181
+ readFileSync(join(root, ".llm-wiki", "raw", "sources", "SRC-OLD", "extracted.md"), "utf8"),
182
+ ).toBe("raw bytes\n");
183
+ expect(readFileSync(join(root, ".llm-wiki", "wiki", "concepts", "legacy.md"), "utf8")).toContain(
184
+ "unknown: keep",
185
+ );
186
+ expect(readFileSync(join(root, ".llm-wiki", "meta", "registry.json"), "utf8")).toBe(
187
+ '{"pages":{}}\n',
188
+ );
189
+ expect(readFileSync(join(root, ".llm-wiki", "outputs", "report.md"), "utf8")).toBe(
190
+ "report bytes\n",
191
+ );
192
+ expect(readFileSync(join(root, ".llm-wiki", ".discoveries", "state.json"), "utf8")).toBe(
193
+ "discovery bytes\n",
194
+ );
195
+ expect(readFileSync(join(root, ".llm-wiki", "WIKI_SCHEMA.md"), "utf8")).toBe(
196
+ "schema bytes\n",
197
+ );
198
+ expect(readFileSync(join(root, ".wiki", "extra.txt"), "utf8")).toBe(
199
+ "leave in old marker directory\n",
200
+ );
201
+ expect(readFileSync(join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md"), "utf8")).toContain(
202
+ "# Migration Complete",
203
+ );
204
+ const after = snapshot(root);
205
+ for (const [source, destination] of Object.entries(migratedPaths)) {
206
+ expect(after[destination], `${source} → ${destination}`).toBe(before[source]);
207
+ expect(after[source], `${source} should be moved`).toBeUndefined();
208
+ }
209
+ expect(after[".wiki/extra.txt"]).toBe(before[".wiki/extra.txt"]);
210
+ expect(existsSync(join(root, "raw"))).toBe(false);
211
+ expect(existsSync(join(root, "wiki"))).toBe(false);
212
+ expect(existsSync(join(root, "meta"))).toBe(false);
213
+ }
214
+
215
+ describe("migrate-llm-wiki CLI", () => {
216
+ it.each(["absolute", "relative"] as const)(
217
+ "dry-runs and applies a legacy migration through a %s path containing spaces",
218
+ (pathMode) => {
219
+ const root = tempRoot();
220
+ seedLegacy(root);
221
+ const cwd = pathMode === "relative" ? dirname(root) : rootDir;
222
+ const rootArg = pathMode === "relative" ? basename(root) : root;
223
+ const before = snapshot(root);
224
+
225
+ const dryRun = runMigration([rootArg, "--dry-run"], cwd);
226
+ expect(dryRun.status, dryRun.stderr).toBe(0);
227
+ expect(dryRun.stdout).toContain(`Scanning for legacy wiki at: ${root}`);
228
+ expect(snapshot(root)).toEqual(before);
229
+
230
+ const apply = runMigration(["--force", rootArg], cwd);
231
+ expect(apply.status, apply.stderr).toBe(0);
232
+ expect(apply.stdout).toContain("Migration complete");
233
+ assertMigrated(root, before);
234
+
235
+ const migrated = snapshot(root);
236
+ const rerun = runMigration([rootArg, "--force"], cwd);
237
+ expect(rerun.status, rerun.stderr).toBe(0);
238
+ expect(rerun.stdout).toContain("New-format wiki already exists");
239
+ expect(snapshot(root)).toEqual(migrated);
240
+ },
241
+ );
242
+
243
+ it("rejects any destination collision before moving legacy files", () => {
244
+ const root = tempRoot();
245
+ seedLegacy(root);
246
+ mkdirSync(join(root, ".llm-wiki", "wiki"), { recursive: true });
247
+ writeFileSync(join(root, ".llm-wiki", "wiki", "existing.md"), "destination bytes\n");
248
+ const before = snapshot(root);
249
+
250
+ const result = runMigration([root, "--force"]);
251
+
252
+ expect(result.status).toBe(1);
253
+ expect(result.stdout).toContain("Migration blocked by destination conflicts");
254
+ expect(result.stdout).toContain(".llm-wiki/wiki");
255
+ expect(snapshot(root)).toEqual(before);
256
+ expect(existsSync(join(root, ".llm-wiki", "config.json"))).toBe(false);
257
+ expect(existsSync(join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md"))).toBe(false);
258
+ });
259
+
260
+ it("never overwrites an existing forwarding marker", () => {
261
+ const root = tempRoot();
262
+ seedLegacy(root);
263
+ writeFileSync(
264
+ join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md"),
265
+ "user-owned marker bytes\n",
266
+ );
267
+ const before = snapshot(root);
268
+
269
+ const result = runMigration([root, "--force"]);
270
+
271
+ expect(result.status).toBe(1);
272
+ expect(result.stdout).toContain("Migration blocked by destination conflicts");
273
+ expect(snapshot(root)).toEqual(before);
274
+ expect(readFileSync(join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md"), "utf8")).toBe(
275
+ "user-owned marker bytes\n",
276
+ );
277
+ expect(existsSync(join(root, ".llm-wiki", "config.json"))).toBe(false);
278
+ });
279
+
280
+ it.each([
281
+ ["config", ".llm-wiki/config.json"],
282
+ ["schema", ".llm-wiki/WIKI_SCHEMA.md"],
283
+ ["forwarding marker", ".wiki/MIGRATED_TO_LLM_WIKI.md"],
284
+ ] as const)("does not overwrite a raced %s destination", async (_label, racedPath) => {
285
+ const root = tempRoot();
286
+ seedLegacy(root);
287
+ const before = snapshot(root);
288
+ const child: ChildProcessWithoutNullStreams = spawn(process.execPath, [script, root], {
289
+ cwd: rootDir,
290
+ stdio: "pipe",
291
+ });
292
+ let stdout = "";
293
+ const confirmation = new Promise<void>((resolve) => {
294
+ child.stdout.on("data", (chunk: Buffer) => {
295
+ stdout += chunk.toString("utf8");
296
+ if (stdout.includes("Proceed with migration?")) resolve();
297
+ });
298
+ });
299
+
300
+ await confirmation;
301
+ const racedDestination = join(root, racedPath);
302
+ mkdirSync(dirname(racedDestination), { recursive: true });
303
+ writeFileSync(racedDestination, "race winner\n");
304
+ child.stdin.end("y\n");
305
+ const code = await new Promise<number | null>((resolve) => child.once("close", resolve));
306
+
307
+ expect(code).toBe(1);
308
+ expect(readFileSync(racedDestination, "utf8")).toBe("race winner\n");
309
+ const after = snapshot(root);
310
+ for (const [source, destination] of Object.entries(migratedPaths)) {
311
+ expect(after[source], `${source} should be restored`).toBe(before[source]);
312
+ if (destination !== racedPath) {
313
+ expect(after[destination], `${destination} should be rolled back`).toBeUndefined();
314
+ }
315
+ }
316
+ if (racedPath !== ".wiki/MIGRATED_TO_LLM_WIKI.md") {
317
+ expect(after[".wiki/MIGRATED_TO_LLM_WIKI.md"]).toBeUndefined();
318
+ }
319
+ });
320
+
321
+ it("does not overwrite a doubled-layout entry raced in after confirmation", async () => {
322
+ const root = tempRoot("pi doubled race ");
323
+ const inner = join(root, ".llm-wiki", ".llm-wiki");
324
+ mkdirSync(join(inner, "meta"), { recursive: true });
325
+ writeFileSync(join(inner, "config.json"), '{"inner":true}\n');
326
+ writeFileSync(join(inner, "meta", "registry.json"), "inner registry\n");
327
+ const child: ChildProcessWithoutNullStreams = spawn(
328
+ process.execPath,
329
+ [script, "--fix-doubled", root],
330
+ { cwd: rootDir, stdio: "pipe" },
331
+ );
332
+ let stdout = "";
333
+ const confirmation = new Promise<void>((resolve) => {
334
+ child.stdout.on("data", (chunk: Buffer) => {
335
+ stdout += chunk.toString("utf8");
336
+ if (stdout.includes("Proceed with flatten?")) resolve();
337
+ });
338
+ });
339
+
340
+ await confirmation;
341
+ writeFileSync(join(root, ".llm-wiki", "config.json"), "race winner\n");
342
+ child.stdin.end("y\n");
343
+ const code = await new Promise<number | null>((resolve) => child.once("close", resolve));
344
+
345
+ expect(code).toBe(0);
346
+ expect(readFileSync(join(root, ".llm-wiki", "config.json"), "utf8")).toBe(
347
+ "race winner\n",
348
+ );
349
+ expect(readFileSync(join(inner, "config.json"), "utf8")).toBe('{"inner":true}\n');
350
+ expect(readFileSync(join(root, ".llm-wiki", "meta", "registry.json"), "utf8")).toBe(
351
+ "inner registry\n",
352
+ );
353
+ });
354
+
355
+ it("dry-runs and applies doubled-layout recovery without overwriting collisions", () => {
356
+ const root = tempRoot("pi doubled migration ");
357
+ const inner = join(root, ".llm-wiki", ".llm-wiki");
358
+ mkdirSync(join(inner, "wiki", "sources"), { recursive: true });
359
+ mkdirSync(join(inner, "meta"), { recursive: true });
360
+ writeFileSync(join(inner, "config.json"), '{"inner":true}\n');
361
+ writeFileSync(join(inner, "wiki", "sources", "note.md"), "inner page\n");
362
+ writeFileSync(join(inner, "meta", "registry.json"), "inner registry\n");
363
+ writeFileSync(join(root, ".llm-wiki", "config.json"), '{"outer":true}\n');
364
+ const before = snapshot(root);
365
+
366
+ const dryRun = runMigration(["--fix-doubled", root, "--dry-run"]);
367
+ expect(dryRun.status, dryRun.stderr).toBe(0);
368
+ expect(snapshot(root)).toEqual(before);
369
+
370
+ const apply = runMigration(["--fix-doubled", root, "--force"]);
371
+ expect(apply.status, apply.stderr).toBe(0);
372
+ expect(readFileSync(join(root, ".llm-wiki", "config.json"), "utf8")).toBe(
373
+ '{"outer":true}\n',
374
+ );
375
+ expect(readFileSync(join(inner, "config.json"), "utf8")).toBe('{"inner":true}\n');
376
+ expect(readFileSync(join(root, ".llm-wiki", "wiki", "sources", "note.md"), "utf8")).toBe(
377
+ "inner page\n",
378
+ );
379
+ expect(readFileSync(join(root, ".llm-wiki", "meta", "registry.json"), "utf8")).toBe(
380
+ "inner registry\n",
381
+ );
382
+ });
383
+ });
384
+ ```
385
+
386
+ - [ ] **Step 3: Run the migration tests and verify the reproduced failures**
387
+
388
+ Run:
389
+
390
+ ```bash
391
+ pnpm vitest run test/migration-script.test.ts
392
+ ```
393
+
394
+ Expected: the absolute-path cases report the wrong scan root; relative apply exits non-zero with `EISDIR`; the conflict case either mutates state or does not emit the expected preflight message.
395
+
396
+ - [ ] **Step 4: Make the standalone script Node 18-compatible and resolve positional roots once**
397
+
398
+ Replace the script's ESM imports with CommonJS built-in imports so the published `.js` file runs under the declared minimum Node 18 runtime without requiring `package.json` to set `type: module`:
399
+
400
+ ```js
401
+ const {
402
+ closeSync,
403
+ existsSync,
404
+ linkSync,
405
+ mkdirSync,
406
+ openSync,
407
+ readdirSync,
408
+ renameSync,
409
+ rmdirSync,
410
+ unlinkSync,
411
+ writeFileSync,
412
+ } = require("node:fs");
413
+ const { homedir } = require("node:os");
414
+ const { dirname, join, resolve } = require("node:path");
415
+ ```
416
+
417
+ Add this helper after the CLI flags:
418
+
419
+ ```js
420
+ function resolvePositionalRoot(defaultRoot) {
421
+ const positional = process.argv.slice(2).find((argument) => !argument.startsWith("--"));
422
+ return positional ? resolve(process.cwd(), positional) : defaultRoot;
423
+ }
424
+ ```
425
+
426
+ Replace doubled-mode root selection with:
427
+
428
+ ```js
429
+ const parentRoot = resolvePositionalRoot(homedir());
430
+ ```
431
+
432
+ Replace legacy-mode root selection with:
433
+
434
+ ```js
435
+ const root = resolvePositionalRoot(process.cwd());
436
+ ```
437
+
438
+ This permits flags before or after absolute or relative paths and removes the `MODULE_TYPELESS_PACKAGE_JSON` warning on newer Node versions.
439
+
440
+ - [ ] **Step 5: Replace `moveDir` with no-clobber file moves and a rollback-capable executor**
441
+
442
+ Replace the current `moveDir` function with:
443
+
444
+ ```js
445
+ function moveNoClobber(item) {
446
+ mkdirSync(dirname(item.dest), { recursive: true });
447
+ if (item.type === "file") {
448
+ // Hard-link creation is atomic and fails with EEXIST instead of replacing a raced file.
449
+ linkSync(item.src, item.dest);
450
+ try {
451
+ unlinkSync(item.src);
452
+ } catch (error) {
453
+ unlinkSync(item.dest);
454
+ throw error;
455
+ }
456
+ return;
457
+ }
458
+ if (existsSync(item.dest)) throw new Error(`Destination appeared: ${item.dest}`);
459
+ renameSync(item.src, item.dest);
460
+ }
461
+
462
+ function restoreMove(item) {
463
+ mkdirSync(dirname(item.src), { recursive: true });
464
+ if (item.type === "file") {
465
+ linkSync(item.dest, item.src);
466
+ unlinkSync(item.dest);
467
+ return;
468
+ }
469
+ if (existsSync(item.src)) throw new Error(`Rollback source exists: ${item.src}`);
470
+ renameSync(item.dest, item.src);
471
+ }
472
+
473
+ function executeMovePlan(plan, newRoot, forwardingMarker, markerContent) {
474
+ const rootExisted = existsSync(newRoot);
475
+ const moved = [];
476
+ let markerHandle;
477
+ let ownsMarker = false;
478
+
479
+ try {
480
+ // Reserve the marker atomically before moving anything. A raced marker is never truncated.
481
+ markerHandle = openSync(forwardingMarker, "wx");
482
+ ownsMarker = true;
483
+ for (const item of plan) {
484
+ log(`MOVE ${item.name}: ${item.src} → ${item.dest}`);
485
+ moveNoClobber(item);
486
+ moved.push(item);
487
+ }
488
+ writeFileSync(markerHandle, markerContent, "utf8");
489
+ closeSync(markerHandle);
490
+ markerHandle = undefined;
491
+ log("CREATE forwarding marker: .wiki/MIGRATED_TO_LLM_WIKI.md");
492
+ } catch (error) {
493
+ const rollbackErrors = [];
494
+ if (markerHandle !== undefined) {
495
+ try {
496
+ closeSync(markerHandle);
497
+ } catch (closeError) {
498
+ rollbackErrors.push(`marker close: ${closeError.message}`);
499
+ }
500
+ markerHandle = undefined;
501
+ }
502
+ if (ownsMarker && existsSync(forwardingMarker)) {
503
+ try {
504
+ unlinkSync(forwardingMarker);
505
+ } catch (unlinkError) {
506
+ rollbackErrors.push(`marker cleanup: ${unlinkError.message}`);
507
+ }
508
+ }
509
+ for (const item of moved.reverse()) {
510
+ try {
511
+ if (existsSync(item.dest) && !existsSync(item.src)) restoreMove(item);
512
+ } catch (rollbackError) {
513
+ rollbackErrors.push(`${item.name}: ${rollbackError.message}`);
514
+ }
515
+ }
516
+ if (!rootExisted) {
517
+ try {
518
+ rmdirSync(newRoot);
519
+ } catch {
520
+ // A non-empty directory is evidence preserved for manual recovery.
521
+ }
522
+ }
523
+ const rollback = rollbackErrors.length
524
+ ? ` Rollback errors: ${rollbackErrors.join("; ")}`
525
+ : " All completed moves were rolled back.";
526
+ throw new Error(`Migration move failed: ${error.message}.${rollback}`);
527
+ }
528
+ }
529
+ ```
530
+
531
+ Regular files (`config.json` and `WIKI_SCHEMA.md`) use atomic hard-link creation plus source unlink, so a raced file destination fails with `EEXIST` rather than being replaced. Directories retain same-filesystem `renameSync`, with a mutation-time destination check and reverse-order rollback. The marker is reserved with `wx`, written through the owned file descriptor, and removed only when this process created it and the migration fails.
532
+
533
+ In the existing `fixDoubled()` execution loop, replace the cached collision-only condition:
534
+
535
+ ```js
536
+ if (p.collision) {
537
+ ```
538
+
539
+ with a mutation-time recheck:
540
+
541
+ ```js
542
+ if (p.collision || existsSync(p.dest)) {
543
+ ```
544
+
545
+ This preserves an outer file or directory created while the confirmation prompt is open instead of passing it to overwrite-capable `renameSync`.
546
+
547
+ - [ ] **Step 6: Preflight the complete legacy move plan before confirmation or mutation**
548
+
549
+ After constructing `moves` and `schemas`, create one executable plan and reject collisions:
550
+
551
+ ```js
552
+ const newRoot = join(root, ".llm-wiki");
553
+ const forwardingMarker = join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md");
554
+ const movePlan = [
555
+ ...moves,
556
+ ...schemas.map((schema) => ({ ...schema, type: "file", name: "WIKI_SCHEMA" })),
557
+ ].filter((item) => existsSync(item.src));
558
+ const conflicts = [
559
+ ...movePlan.filter((item) => existsSync(item.dest)),
560
+ ...(existsSync(forwardingMarker)
561
+ ? [{ dest: forwardingMarker, name: "forwarding marker" }]
562
+ : []),
563
+ ];
564
+
565
+ if (conflicts.length > 0) {
566
+ console.log("\n❌ Migration blocked by destination conflicts:");
567
+ for (const conflict of conflicts) console.log(` ${conflict.dest}`);
568
+ console.log(" No files were moved. Resolve these paths and rerun the migration.");
569
+ process.exit(1);
570
+ }
571
+ ```
572
+
573
+ Before execution, build the marker content without writing it:
574
+
575
+ ```js
576
+ const markerContent = [
577
+ "# Migration Complete",
578
+ "",
579
+ `This vault was migrated to the new layout at \`.llm-wiki/\` on ${new Date().toISOString().split("T")[0]}.`,
580
+ "",
581
+ "The old `.wiki/` directory is kept as a forwarding marker.",
582
+ "Remove it once you've verified everything works.",
583
+ "",
584
+ `New location: \`${newRoot}\``,
585
+ "",
586
+ ].join("\n");
587
+ ```
588
+
589
+ Delete the old `moveDir` loop, separate schema loop, and separate forwarding-marker `writeFileSync` block. Replace them with:
590
+
591
+ ```js
592
+ if (!DRY_RUN) executeMovePlan(movePlan, newRoot, forwardingMarker, markerContent);
593
+ else {
594
+ for (const item of movePlan) log(`MOVE ${item.name}: ${item.src} → ${item.dest}`);
595
+ }
596
+ ```
597
+
598
+ Keep missing optional sources represented as `○` in the printed plan. The executor now owns marker creation and rolls moves back if marker reservation or writing fails.
599
+
600
+ - [ ] **Step 7: Run focused migration and existing doubled-layout tests**
601
+
602
+ Run:
603
+
604
+ ```bash
605
+ pnpm vitest run test/migration-script.test.ts test/personal-wiki-paths.test.ts
606
+ ```
607
+
608
+ Expected: all migration CLI tests and all 9 existing helper tests pass. No test touches the real home vault.
609
+
610
+ - [ ] **Step 8: Commit the migration safety fix**
611
+
612
+ Run:
613
+
614
+ ```bash
615
+ git add scripts/migrate-llm-wiki.js test/migration-script.test.ts
616
+ git commit -m "fix: make vault layout migration fail safely"
617
+ ```
618
+
619
+ Expected: one commit containing only the migration script and its black-box tests.
620
+
621
+ ---
622
+
623
+ ### Task 2: Publish the advertised migration script
624
+
625
+ **Files:**
626
+ - Modify: `package.json:36-47`
627
+ - Modify: `test/migration-script.test.ts`
628
+ - Modify: `.github/workflows/ci.yml`
629
+
630
+ - [ ] **Step 1: Add a failing packed-artifact assertion**
631
+
632
+ Append this test inside `describe("migrate-llm-wiki CLI", ...)`:
633
+
634
+ ```ts
635
+ it("includes the advertised migration script in the npm package", () => {
636
+ const packed = spawnSync(
637
+ "npm",
638
+ ["pack", "--dry-run", "--json", "--ignore-scripts"],
639
+ { cwd: rootDir, encoding: "utf8" },
640
+ );
641
+ expect(packed.status, packed.stderr).toBe(0);
642
+ const report = JSON.parse(packed.stdout) as Array<{ files: Array<{ path: string }> }>;
643
+ expect(report[0].files.map((file) => file.path)).toContain("scripts/migrate-llm-wiki.js");
644
+ });
645
+ ```
646
+
647
+ - [ ] **Step 2: Run the packed-artifact test and verify failure**
648
+
649
+ Run:
650
+
651
+ ```bash
652
+ pnpm vitest run test/migration-script.test.ts -t "includes the advertised migration script"
653
+ ```
654
+
655
+ Expected: FAIL because `package.json.files` excludes `scripts/migrate-llm-wiki.js`.
656
+
657
+ - [ ] **Step 3: Include only the user-facing migration script**
658
+
659
+ Add this exact entry to `package.json.files` after `prompts`:
660
+
661
+ ```json
662
+ "scripts/migrate-llm-wiki.js",
663
+ ```
664
+
665
+ Do not include the whole `scripts/` directory; `release.js` and build internals are not runtime package files.
666
+
667
+ - [ ] **Step 4: Verify the packed artifact**
668
+
669
+ Run:
670
+
671
+ ```bash
672
+ pnpm vitest run test/migration-script.test.ts -t "includes the advertised migration script"
673
+ npm pack --dry-run --json | node -e '
674
+ let data="";
675
+ process.stdin.on("data", chunk => data += chunk);
676
+ process.stdin.on("end", () => {
677
+ const files = JSON.parse(data)[0].files.map(file => file.path);
678
+ if (!files.includes("scripts/migrate-llm-wiki.js")) process.exit(1);
679
+ console.log("migration script packaged");
680
+ });'
681
+ ```
682
+
683
+ Expected: test passes and command prints `migration script packaged`.
684
+
685
+ - [ ] **Step 5: Execute the packed script on the minimum supported Node version in CI**
686
+
687
+ Append this job under `jobs:` in `.github/workflows/ci.yml`, alongside `quality`:
688
+
689
+ ```yaml
690
+ migration-node18:
691
+ runs-on: ubuntu-latest
692
+ steps:
693
+ - uses: actions/checkout@v4
694
+
695
+ - uses: actions/setup-node@v4
696
+ with:
697
+ node-version: 18
698
+
699
+ - name: Pack and execute migration on Node 18
700
+ shell: bash
701
+ run: |
702
+ set -euo pipefail
703
+ fixture="$RUNNER_TEMP/legacy vault"
704
+ unpacked="$RUNNER_TEMP/unpacked"
705
+ mkdir -p "$fixture/.wiki" "$fixture/wiki/concepts" "$unpacked"
706
+ printf '{"name":"Node 18 fixture"}\n' > "$fixture/.wiki/config.json"
707
+ printf '%s\n' '---' 'type: concept' '---' '' 'Node 18 body.' > "$fixture/wiki/concepts/node-18.md"
708
+ config_hash=$(sha256sum "$fixture/.wiki/config.json" | cut -d' ' -f1)
709
+ page_hash=$(sha256sum "$fixture/wiki/concepts/node-18.md" | cut -d' ' -f1)
710
+
711
+ tarball=$(npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP" --silent)
712
+ tar -xzf "$RUNNER_TEMP/$tarball" -C "$unpacked"
713
+ node "$unpacked/package/scripts/migrate-llm-wiki.js" "$fixture" --force
714
+
715
+ test -f "$fixture/.llm-wiki/config.json"
716
+ test -f "$fixture/.llm-wiki/wiki/concepts/node-18.md"
717
+ test "$(sha256sum "$fixture/.llm-wiki/config.json" | cut -d' ' -f1)" = "$config_hash"
718
+ test "$(sha256sum "$fixture/.llm-wiki/wiki/concepts/node-18.md" | cut -d' ' -f1)" = "$page_hash"
719
+ ```
720
+
721
+ This job uses no installed dependencies: the migration script must run from the real packed tarball using Node built-ins only.
722
+
723
+ - [ ] **Step 6: Validate the workflow and package tests**
724
+
725
+ Run:
726
+
727
+ ```bash
728
+ pnpm lint
729
+ pnpm vitest run test/migration-script.test.ts
730
+ ```
731
+
732
+ Expected: Biome accepts the workflow and all migration/package tests pass locally. The Node 18 packed execution is required to pass in PR CI.
733
+
734
+ - [ ] **Step 7: Commit the package contract**
735
+
736
+ Run:
737
+
738
+ ```bash
739
+ git add package.json test/migration-script.test.ts .github/workflows/ci.yml
740
+ git commit -m "fix: publish the vault migration script"
741
+ ```
742
+
743
+ ---
744
+
745
+ ### Task 3: Stop generating unresolved first-party source links
746
+
747
+ **Files:**
748
+ - Modify: `extensions/llm-wiki/lib/source-packet.ts:272-309`
749
+ - Modify: `extensions/llm-wiki/lib/ingest-worker.ts:137-184`
750
+ - Modify: `test/source-capture.test.ts:1-70`
751
+ - Modify: `test/ingest-worker.test.ts:1-110`
752
+
753
+ - [ ] **Step 1: Add a skeleton-page regression test**
754
+
755
+ Add this import to `test/source-capture.test.ts`:
756
+
757
+ ```ts
758
+ import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
759
+ ```
760
+
761
+ Add this test after the text-capture test:
762
+
763
+ ```ts
764
+ it("emits no unresolved first-party links in a captured skeleton page", () => {
765
+ const paths = makePaths();
766
+ const result = captureText(paths, "Some text content", "My Note");
767
+ const sourcePage = readFile(result.sourcePagePath);
768
+
769
+ expect(sourcePage).toContain(`\`raw/sources/${result.sourceId}/extracted.md\``);
770
+ expect(sourcePage).toContain(`\`raw/sources/${result.sourceId}/manifest.json\``);
771
+ expect(sourcePage).not.toContain("](/entities/entity-name.md)");
772
+ expect(sourcePage).not.toContain("](/concepts/concept-name.md)");
773
+
774
+ const projection = rebuildMetadata(paths);
775
+ expect(
776
+ projection.diagnostics.filter((diagnostic) =>
777
+ ["link_unresolved", "link_path_escape"].includes(diagnostic.code),
778
+ ),
779
+ ).toEqual([]);
780
+ });
781
+ ```
782
+
783
+ - [ ] **Step 2: Add an ingested-page regression test**
784
+
785
+ Add this import to `test/ingest-worker.test.ts`:
786
+
787
+ ```ts
788
+ import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
789
+ ```
790
+
791
+ Extend `writes the source page (ingested) and creates entity/concept pages` with:
792
+
793
+ ```ts
794
+ expect(sourcePage).toContain("`raw/sources/SRC-001/extracted.md`");
795
+ expect(sourcePage).toContain("`raw/sources/SRC-001/manifest.json`");
796
+ expect(sourcePage).not.toContain("](../raw/");
797
+
798
+ const projection = rebuildMetadata(paths);
799
+ expect(
800
+ projection.diagnostics.filter((diagnostic) =>
801
+ ["link_unresolved", "link_path_escape"].includes(diagnostic.code),
802
+ ),
803
+ ).toEqual([]);
804
+ ```
805
+
806
+ The existing assertions for `/entities/*.md` and `/concepts/*.md` remain unchanged; those are valid knowledge links and must continue producing backlinks.
807
+
808
+ - [ ] **Step 3: Run both focused tests and verify failure**
809
+
810
+ Run:
811
+
812
+ ```bash
813
+ pnpm vitest run test/source-capture.test.ts test/ingest-worker.test.ts
814
+ ```
815
+
816
+ Expected: failures show `../raw/...` Markdown links and unresolved `entity-name`/`concept-name` placeholders.
817
+
818
+ - [ ] **Step 4: Render skeleton placeholders as text, not fake links**
819
+
820
+ In `buildSourcePageSkeleton()` within `extensions/llm-wiki/lib/source-packet.ts`, replace the entity/concept placeholder sections with:
821
+
822
+ ```md
823
+ ## Entities Mentioned
824
+
825
+ - [LLM: Add linked entities after review]
826
+
827
+ ## Concepts Mentioned
828
+
829
+ - [LLM: Add linked concepts after review]
830
+ ```
831
+
832
+ A bracketed placeholder without a destination is plain CommonMark text and does not create a backlink candidate.
833
+
834
+ - [ ] **Step 5: Render raw artifacts as extension paths, not OKF links**
835
+
836
+ In both `buildSourcePageSkeleton()` and `buildIngestedSourcePageBody()`, replace the Source Packet rows with:
837
+
838
+ ```md
839
+ - **ID:** `sources/${id}`
840
+ - **Extracted:** `raw/sources/${id}/extracted.md`
841
+ - **Manifest:** `raw/sources/${id}/manifest.json`
842
+ ```
843
+
844
+ Raw packets live outside the portable `wiki/` OKF bundle. Code paths preserve useful extension metadata without pretending they are resolvable concept links. Keep `raw_path` frontmatter unchanged.
845
+
846
+ - [ ] **Step 6: Run source, ingestion, backlink, and projection tests**
847
+
848
+ Run:
849
+
850
+ ```bash
851
+ pnpm vitest run \
852
+ test/source-capture.test.ts \
853
+ test/ingest-worker.test.ts \
854
+ test/knowledge-links.test.ts \
855
+ test/okf-projections.test.ts \
856
+ test/okf-integration.test.ts
857
+ ```
858
+
859
+ Expected: all tests pass; captured and ingested source pages produce no unexpected link diagnostics; entity/concept backlinks remain present.
860
+
861
+ - [ ] **Step 7: Commit the generated-link fix**
862
+
863
+ Run:
864
+
865
+ ```bash
866
+ git add \
867
+ extensions/llm-wiki/lib/source-packet.ts \
868
+ extensions/llm-wiki/lib/ingest-worker.ts \
869
+ test/source-capture.test.ts \
870
+ test/ingest-worker.test.ts
871
+ git commit -m "fix: stop emitting unresolved source page links"
872
+ ```
873
+
874
+ ---
875
+
876
+ ### Task 4: Stabilize the MCP output-cap stress test without weakening coverage
877
+
878
+ **Files:**
879
+ - Modify: `test/mcp-exec.test.ts:45-112`
880
+ - Modify: `test/migration-script.test.ts:295-304`
881
+
882
+ - [ ] **Step 1: Keep the child alive until the cap fires and increase only this stress-case deadline**
883
+
884
+ Replace the current parameterized output-cap test with:
885
+
886
+ ```ts
887
+ it.each(["stdout", "stderr"] as const)(
888
+ "bounds captured %s at a complete UTF-8 code point",
889
+ async (stream) => {
890
+ const script =
891
+ stream === "stdout"
892
+ ? "process.on('SIGTERM',()=>{});process.stdout.write('x'.repeat(16*1024*1024-1)+'€',()=>setTimeout(()=>process.stdout.write('A'),10));setInterval(()=>{},1000)"
893
+ : "process.on('SIGTERM',()=>{});process.stderr.write('x'.repeat(16*1024*1024-1)+'€',()=>setTimeout(()=>process.stderr.write('A'),10));setInterval(()=>{},1000)";
894
+ const result = await createExecApi().exec(process.execPath, ["-e", script], {
895
+ timeout: 15_000,
896
+ });
897
+ expect(result).toMatchObject({ killed: true, code: 1 });
898
+ expect(Buffer.byteLength(result[stream])).toBe(16 * 1024 * 1024 - 1);
899
+ },
900
+ 20_000,
901
+ );
902
+ ```
903
+
904
+ The interval prevents the fixture process from exiting merely because a five-second keepalive elapsed under load; the parent still kills it as soon as the production cap is reached, with a 15-second fallback timeout. Do not lower the 16 MiB payload and do not remove the exact UTF-8 byte assertion. The production cap remains unchanged.
905
+
906
+ - [ ] **Step 2: Remove the other load-sensitive process and package-test deadlines**
907
+
908
+ Increase the descendant-process fixture command timeout in `test/mcp-exec.test.ts` from `100` to `1_000` milliseconds so Node can synchronously write `child.pid` before the process group is killed under parallel load. Keep the existing post-kill descendant assertion unchanged.
909
+
910
+ Give the real `npm pack --dry-run` test in `test/migration-script.test.ts` an explicit `30_000` millisecond Vitest timeout. Keep the package-content assertion unchanged.
911
+
912
+ - [ ] **Step 3: Exercise the stress tests repeatedly and under the full worker pool**
913
+
914
+ Run:
915
+
916
+ ```bash
917
+ for run in 1 2 3; do
918
+ pnpm vitest run test/mcp-exec.test.ts
919
+ done
920
+ pnpm test
921
+ ```
922
+
923
+ Expected: all three focused runs pass, followed by all 542+ repository tests under normal parallel execution.
924
+
925
+ - [ ] **Step 4: Commit the test stabilization**
926
+
927
+ Run:
928
+
929
+ ```bash
930
+ git add \
931
+ docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md \
932
+ test/mcp-exec.test.ts \
933
+ test/migration-script.test.ts
934
+ git commit -m "test: stabilize release gate process coverage"
935
+ ```
936
+
937
+ ---
938
+
939
+ ### Task 5: Document the fixes and run the full release gate
940
+
941
+ **Files:**
942
+ - Modify: `CHANGELOG.md:3-10`
943
+
944
+ - [ ] **Step 1: Add exact `Unreleased` changelog entries**
945
+
946
+ Add these bullets under `## [Unreleased]` → `### Fixed`:
947
+
948
+ ```markdown
949
+ - **Vault layout migration safety and packaging**: `scripts/migrate-llm-wiki.js` now runs on the minimum supported Node 18 runtime, accepts absolute or relative roots regardless of flag order, uses no-clobber file moves and an exclusively reserved forwarding marker, rejects destination collisions before moving data, rolls back completed moves after synchronous failures, and ships in the npm package. Black-box tests cover paths with spaces, dry-run immutability, hash-preserving apply, idempotency, preflight and raced config/schema/marker collisions, doubled-layout recovery, packed-artifact contents, and packed execution on Node 18.
950
+ - **Generated source pages emitted unresolved first-party links**: source skeletons no longer create fake entity/concept links, and raw packet artifacts are rendered as extension-owned code paths rather than Markdown links inside the OKF bundle. Captured and ingested pages now rebuild without unexpected `link_unresolved` or `link_path_escape` diagnostics while retaining valid entity/concept backlinks.
951
+ - **MCP output-cap stress test was load-sensitive**: the real 16 MiB stdout/stderr UTF-8 boundary test keeps its exact byte assertions but now allows enough time under parallel coverage load.
952
+ ```
953
+
954
+ Do not claim that OKF content migration, import, or export exists.
955
+
956
+ - [ ] **Step 2: Run formatting, type, and whitespace checks**
957
+
958
+ Run:
959
+
960
+ ```bash
961
+ pnpm typecheck
962
+ pnpm lint
963
+ git diff --check
964
+ ```
965
+
966
+ Expected: all commands exit 0 with no fixes or whitespace errors.
967
+
968
+ - [ ] **Step 3: Run focused Foundation and migration suites**
969
+
970
+ Run:
971
+
972
+ ```bash
973
+ pnpm vitest run --maxWorkers=1 \
974
+ test/migration-script.test.ts \
975
+ test/personal-wiki-paths.test.ts \
976
+ test/source-capture.test.ts \
977
+ test/ingest-worker.test.ts \
978
+ test/knowledge-document.test.ts \
979
+ test/knowledge-links.test.ts \
980
+ test/vault-format.test.ts \
981
+ test/okf-projections.test.ts \
982
+ test/okf-integration.test.ts \
983
+ test/mcp-parity.test.ts \
984
+ test/mcp-exec.test.ts \
985
+ test/mcp-package.test.ts
986
+ ```
987
+
988
+ Expected: every focused file passes.
989
+
990
+ - [ ] **Step 4: Run the full coverage release gate**
991
+
992
+ Run:
993
+
994
+ ```bash
995
+ pnpm test:coverage
996
+ ```
997
+
998
+ Expected: all test files and tests pass with configured global and trusted-boundary thresholds. No output-cap timeout is tolerated.
999
+
1000
+ - [ ] **Step 5: Inspect the actual package contract**
1001
+
1002
+ Run:
1003
+
1004
+ ```bash
1005
+ pnpm build:mcp
1006
+ npm pack --dry-run --json > /tmp/pi-llm-wiki-pack.json
1007
+ node - <<'NODE'
1008
+ const report = require("/tmp/pi-llm-wiki-pack.json")[0];
1009
+ const files = new Set(report.files.map((file) => file.path));
1010
+ for (const required of ["scripts/migrate-llm-wiki.js", "dist/mcp/index.js"]) {
1011
+ if (!files.has(required)) throw new Error(`missing packed file: ${required}`);
1012
+ }
1013
+ console.log(`package files: ${report.entryCount}; migration and MCP entries present`);
1014
+ NODE
1015
+ ```
1016
+
1017
+ Expected: command prints package count and confirms both runtime entries.
1018
+
1019
+ - [ ] **Step 6: Commit release documentation**
1020
+
1021
+ Run:
1022
+
1023
+ ```bash
1024
+ git add CHANGELOG.md
1025
+ git commit -m "docs: record OKF release gate fixes"
1026
+ ```
1027
+
1028
+ ---
1029
+
1030
+ ### Task 6: Review the final range and raise the PR
1031
+
1032
+ **Files:**
1033
+ - Review only: all files changed since `origin/main`
1034
+
1035
+ - [ ] **Step 1: Verify the final branch range**
1036
+
1037
+ Run:
1038
+
1039
+ ```bash
1040
+ git status --short
1041
+ git log --oneline origin/main..HEAD
1042
+ git diff --stat origin/main...HEAD
1043
+ git diff --check origin/main...HEAD
1044
+ ```
1045
+
1046
+ Expected: clean working tree, six focused commits (plan, migration, packaging, generated links, MCP test, changelog), only planned files changed, and no whitespace errors.
1047
+
1048
+ - [ ] **Step 2: Run an independent bug review**
1049
+
1050
+ Invoke the repository `code_review` tool with:
1051
+
1052
+ ```json
1053
+ {
1054
+ "branch": "origin/main",
1055
+ "lenses": ["correctness", "security", "tests"]
1056
+ }
1057
+ ```
1058
+
1059
+ Required review focus:
1060
+
1061
+ - migration never overwrites destination entries silently;
1062
+ - absolute, relative, and space-containing paths resolve correctly;
1063
+ - config and schema destinations remain files;
1064
+ - normal apply is byte-preserving and rerunnable;
1065
+ - raw packet paths are not interpreted as OKF concept links;
1066
+ - valid entity/concept backlinks still exist;
1067
+ - package contains the advertised script and its packed copy executes on Node 18;
1068
+ - config, schema, marker, and doubled-layout race fixtures never overwrite competing bytes;
1069
+ - no `wiki_okf_migrate` or other later-phase surface was added.
1070
+
1071
+ Expected: no Critical or Important findings. Fix any finding and rerun the relevant focused plus full gates before proceeding.
1072
+
1073
+ - [ ] **Step 3: Push the branch**
1074
+
1075
+ Run:
1076
+
1077
+ ```bash
1078
+ git push -u origin fix/okf-foundation-release-gate
1079
+ ```
1080
+
1081
+ Expected: remote branch created successfully.
1082
+
1083
+ - [ ] **Step 4: Open the pull request**
1084
+
1085
+ Run:
1086
+
1087
+ ```bash
1088
+ gh pr create \
1089
+ --base main \
1090
+ --head fix/okf-foundation-release-gate \
1091
+ --title "fix: clear OKF Foundation release blockers" \
1092
+ --body-file - <<'EOF'
1093
+ ## Summary
1094
+
1095
+ - make the legacy `.wiki` → `.llm-wiki` migration path-safe, conflict-safe, rollback-capable, and publishable
1096
+ - stop generated source pages from emitting unresolved raw-artifact and placeholder links
1097
+ - stabilize the MCP 16 MiB UTF-8 output-cap stress test under parallel coverage
1098
+ - add black-box migration, packed-artifact, and generated-link regression coverage
1099
+
1100
+ ## Scope
1101
+
1102
+ This PR remediates the OKF Foundation release gate. It does not add OKF content migration, import, export, or `wiki_okf_migrate`; those remain later Interchange work.
1103
+
1104
+ ## Verification
1105
+
1106
+ - `pnpm typecheck`
1107
+ - `pnpm lint`
1108
+ - `pnpm test:coverage`
1109
+ - `pnpm build:mcp`
1110
+ - `npm pack --dry-run --json`
1111
+ - real CLI fixtures for absolute/relative paths, spaces, dry-run, hash-preserving apply, idempotency, preflight/raced collisions, doubled-layout recovery, and packed Node 18 execution
1112
+ EOF
1113
+ ```
1114
+
1115
+ Expected: GitHub returns a PR URL targeting `main`.
1116
+
1117
+ - [ ] **Step 5: Wait for and verify CI before merge recommendation**
1118
+
1119
+ Run:
1120
+
1121
+ ```bash
1122
+ gh pr checks --watch
1123
+ ```
1124
+
1125
+ Expected: every required Node, CodeQL, lint, and package check passes. Record the exact PR head SHA and do not recommend merge if the checked SHA differs from branch HEAD.
1126
+
1127
+ ---
1128
+
1129
+ ## Live-vault acceptance addendum
1130
+
1131
+ A guarded acceptance run against the real 634-file personal vault found two release gaps that fixture-only validation missed:
1132
+
1133
+ 1. Process interruption after the first move leaves a partial layout with no restart protocol.
1134
+ 2. Fifteen historical pages are not accepted by the strict parser (seven missing frontmatter, two missing `type`, and six invalid YAML), so metadata rebuild blocks even though the existing registry still supports reads.
1135
+
1136
+ ### Task 6: Make layout migration resumable
1137
+
1138
+ - [x] Persist a durable migration journal before the first move.
1139
+ - [x] Reserve the destination root atomically after confirmation.
1140
+ - [x] Hash each planned source and verify each resumed destination before trusting it.
1141
+ - [x] Recover the hard-link-created/before-source-unlink crash state.
1142
+ - [x] Flush journal, marker, and moved directory entries at durability boundaries.
1143
+ - [x] Add a real subprocess `SIGKILL` test that resumes and verifies every original byte.
1144
+
1145
+ ### Task 7: Repair malformed legacy pages through the existing lint surface
1146
+
1147
+ - [x] Keep `wiki_lint auto_fix=false` read-only and report projection blockers.
1148
+ - [x] On explicit `auto_fix=true`, repair only recoverable legacy frontmatter failures.
1149
+ - [x] Back up every original page under `outputs/legacy-repair-*/wiki/` before replacement.
1150
+ - [x] Record old/new SHA-256 values and diagnostics in a repair manifest.
1151
+ - [x] Preserve valid legacy metadata, archive unparseable raw frontmatter, and retain page bodies.
1152
+ - [x] Rebuild projections only after every repaired page parses successfully.
1153
+ - [x] Repeat the guarded live-vault migration, repair, OKF projection, real Pi tool, injection, MCP, restart, and interruption acceptance run.
1154
+
1155
+ ---
1156
+
1157
+ ## Acceptance criteria
1158
+
1159
+ The PR is ready for merge only when all statements below are true:
1160
+
1161
+ 1. `node scripts/migrate-llm-wiki.js /absolute/path --force` and the equivalent relative path work, including paths containing spaces.
1162
+ 2. Dry-run leaves every fixture byte-identical.
1163
+ 3. Successful apply preserves config, templates, raw packets, wiki pages, metadata, outputs, discoveries, schema, and additional `.wiki/` files.
1164
+ 4. Any destination collision, including an existing forwarding marker, aborts before the first source move and preserves existing bytes.
1165
+ 5. Config, schema, or marker destinations introduced after preflight are never overwritten; any completed moves roll back, competing bytes remain untouched, and no migration-owned marker remains after failure.
1166
+ 6. `config.json` and `WIKI_SCHEMA.md` are regular files after migration.
1167
+ 7. A successful rerun is a no-op.
1168
+ 8. Doubled-layout automatic/helper behavior remains green, and both pre-existing and raced CLI collisions preserve outer entries.
1169
+ 9. `npm pack --dry-run --json` contains `scripts/migrate-llm-wiki.js` and `dist/mcp/index.js`, and the packed migration script completes on Node 18.
1170
+ 10. Captured skeleton and ingested source pages produce no unexpected `link_unresolved` or `link_path_escape` diagnostics.
1171
+ 11. Entity and concept Markdown links still resolve and generate backlinks.
1172
+ 12. MCP output-cap tests retain the 16 MiB limit and exact complete-code-point assertion and pass under normal parallel coverage.
1173
+ 13. Typecheck, lint, full tests, coverage thresholds, MCP build, package smoke, and independent review pass.
1174
+ 14. The PR contains no package-version edit and no later-phase OKF content-migration surface.