pi-harness-runtime 1.1.324 → 1.1.326

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 (125) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/dist/policy-loader.d.ts +8 -1
  4. package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -1
  5. package/packages/agent-policy/dist/policy-loader.js +23 -5
  6. package/packages/agent-policy/dist/policy-loader.js.map +1 -1
  7. package/packages/agent-policy/package.json +1 -1
  8. package/packages/agent-policy/src/policy-loader.ts +28 -7
  9. package/packages/agent-policy/test/policy-retention.test.ts +43 -1
  10. package/packages/agent-policy/test/policy.test.ts +7 -3
  11. package/packages/architecture-generator/package.json +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/autonomous-refactor/package.json +1 -1
  14. package/packages/autonomous-runtime/package.json +1 -1
  15. package/packages/cache-strategy/package.json +1 -1
  16. package/packages/capability-registry/package.json +1 -1
  17. package/packages/checkpoint/package.json +1 -1
  18. package/packages/cli-plugin-sdk/package.json +1 -1
  19. package/packages/clipboard/package.json +1 -1
  20. package/packages/clipboard-plugin/package.json +1 -1
  21. package/packages/code-generation/package.json +1 -1
  22. package/packages/code-review/package.json +1 -1
  23. package/packages/codex-adapter/package.json +1 -1
  24. package/packages/codex-watcher/package.json +1 -1
  25. package/packages/config-capture/package.json +1 -1
  26. package/packages/context-compiler/package.json +1 -1
  27. package/packages/context-discovery/package.json +1 -1
  28. package/packages/context-manager/package.json +1 -1
  29. package/packages/cookie-sanitizer/package.json +1 -1
  30. package/packages/cost-optimizer/package.json +1 -1
  31. package/packages/dependency-analyzer/package.json +1 -1
  32. package/packages/django-plugin/package.json +1 -1
  33. package/packages/doc-generator/package.json +1 -1
  34. package/packages/durable-review/package.json +1 -1
  35. package/packages/e2e-tester/package.json +1 -1
  36. package/packages/evaluation-engine/package.json +1 -1
  37. package/packages/evaluation-runner/package.json +1 -1
  38. package/packages/event-bus/package.json +1 -1
  39. package/packages/event-store/package.json +1 -1
  40. package/packages/experience-replay/package.json +1 -1
  41. package/packages/feedback-collector/package.json +1 -1
  42. package/packages/file-copy-helper/package.json +1 -1
  43. package/packages/framework-detector/package.json +1 -1
  44. package/packages/framework-plugin-sdk/package.json +1 -1
  45. package/packages/frappe-plugin/package.json +1 -1
  46. package/packages/generic-web-plugin/package.json +1 -1
  47. package/packages/health-monitor/package.json +1 -1
  48. package/packages/intent-analyzer/package.json +1 -1
  49. package/packages/jev-judge/package.json +1 -1
  50. package/packages/knowledge-graph/package.json +1 -1
  51. package/packages/knowledge-retrieval/package.json +1 -1
  52. package/packages/laravel-plugin/package.json +1 -1
  53. package/packages/learning-engine/package.json +1 -1
  54. package/packages/mcp-adapter/package.json +1 -1
  55. package/packages/memory-engine/package.json +1 -1
  56. package/packages/milestone-manager/package.json +1 -1
  57. package/packages/model-registry/package.json +1 -1
  58. package/packages/nextjs-plugin/package.json +1 -1
  59. package/packages/notification/package.json +1 -1
  60. package/packages/observability/package.json +1 -1
  61. package/packages/okf-indexer/package.json +1 -1
  62. package/packages/paperclip-integration/package.json +1 -1
  63. package/packages/performance-optimizer/package.json +1 -1
  64. package/packages/privilege-broker/package.json +1 -1
  65. package/packages/project-analyzer/package.json +1 -1
  66. package/packages/project-bootstrap/package.json +1 -1
  67. package/packages/projection-engine/package.json +1 -1
  68. package/packages/prompt-compiler/package.json +1 -1
  69. package/packages/prompt-versioning/package.json +1 -1
  70. package/packages/provider-adapter-sdk/package.json +1 -1
  71. package/packages/provider-router/package.json +1 -1
  72. package/packages/provider-selector/package.json +1 -1
  73. package/packages/providers/package.json +1 -1
  74. package/packages/qdrant-skills/package.json +1 -1
  75. package/packages/quota-manager/package.json +1 -1
  76. package/packages/rate-limiter/package.json +1 -1
  77. package/packages/react-vite-plugin/package.json +1 -1
  78. package/packages/release-manager/package.json +1 -1
  79. package/packages/requirement-compiler/package.json +1 -1
  80. package/packages/runtime/package.json +1 -1
  81. package/packages/scheduler/package.json +1 -1
  82. package/packages/scheduler-adapter/package.json +1 -1
  83. package/packages/session/package.json +1 -1
  84. package/packages/session-api/package.json +1 -1
  85. package/packages/session-export/package.json +1 -1
  86. package/packages/shared-context/package.json +1 -1
  87. package/packages/skill-mcp-client/package.json +1 -1
  88. package/packages/skill-registry/package.json +1 -1
  89. package/packages/skills/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/telegram/package.json +1 -1
  94. package/packages/tencentdb-memory/package.json +1 -1
  95. package/packages/tencentdb-sync/package.json +1 -1
  96. package/packages/test-data-generator/package.json +1 -1
  97. package/packages/test-generator/package.json +1 -1
  98. package/packages/todo-bd-sync/package.json +1 -1
  99. package/packages/token-estimation/package.json +1 -1
  100. package/packages/token-optimizer/package.json +1 -1
  101. package/packages/trajectory/package.json +1 -1
  102. package/packages/tui/package.json +1 -1
  103. package/packages/types/package.json +1 -1
  104. package/packages/webmcp-tester/package.json +1 -1
  105. package/packages/wiki/dist/index.d.ts +132 -0
  106. package/packages/wiki/dist/index.d.ts.map +1 -0
  107. package/packages/wiki/dist/index.js +448 -0
  108. package/packages/wiki/dist/index.js.map +1 -0
  109. package/packages/wiki/dist/storage.d.ts +142 -0
  110. package/packages/wiki/dist/storage.d.ts.map +1 -0
  111. package/packages/wiki/dist/storage.js +603 -0
  112. package/packages/wiki/dist/storage.js.map +1 -0
  113. package/packages/wiki/dist/types.d.ts +189 -0
  114. package/packages/wiki/dist/types.d.ts.map +1 -0
  115. package/packages/wiki/dist/types.js +30 -0
  116. package/packages/wiki/dist/types.js.map +1 -0
  117. package/packages/wiki/package.json +22 -0
  118. package/packages/wiki/src/index.ts +578 -0
  119. package/packages/wiki/src/storage.ts +696 -0
  120. package/packages/wiki/src/types.ts +263 -0
  121. package/packages/wiki/test/basic.test.ts +242 -0
  122. package/packages/workflow-events/package.json +1 -1
  123. package/packages/workspace-scanner/package.json +1 -1
  124. package/packages/worktree/package.json +1 -1
  125. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Wiki Storage - File-based persistence for wiki pages
3
+ *
4
+ * Handles atomic writes, transactions, and consistency.
5
+ */
6
+
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ unlinkSync,
13
+ renameSync,
14
+ readdirSync,
15
+ statSync,
16
+ } from "node:fs";
17
+ import { join, dirname, relative } from "node:path";
18
+ import { createHash, randomUUID } from "node:crypto";
19
+ import type {
20
+ WikiConfig,
21
+ WikiPage,
22
+ WikiPageFrontmatter,
23
+ SourceSnapshot,
24
+ WikiOperation,
25
+ PageIndexEntry,
26
+ WikiInitResult,
27
+ WikiOperationType,
28
+ WikiSearchFilters,
29
+ WikiSearchResult,
30
+ LintFinding,
31
+ StagedChange,
32
+ WIKI_DIRS,
33
+ } from "./types.js";
34
+
35
+ // ============================================================================
36
+ // Utilities
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Compute SHA-256 hash of content
41
+ */
42
+ export function computeHash(content: string): string {
43
+ return createHash("sha256").update(content, "utf8").digest("hex");
44
+ }
45
+
46
+ /**
47
+ * Get current ISO timestamp
48
+ */
49
+ export function now(): string {
50
+ return new Date().toISOString();
51
+ }
52
+
53
+ /**
54
+ * Parse YAML frontmatter from markdown content
55
+ */
56
+ export function parseFrontmatter(
57
+ content: string
58
+ ): { frontmatter: WikiPageFrontmatter | null; body: string; raw: string } {
59
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
60
+
61
+ if (!match) {
62
+ return { frontmatter: null, body: content, raw: "" };
63
+ }
64
+
65
+ const [, yamlStr, body] = match;
66
+ const frontmatter = parseYaml(yamlStr);
67
+
68
+ return {
69
+ frontmatter: frontmatter as unknown as WikiPageFrontmatter | null,
70
+ body: body.trim(),
71
+ raw: yamlStr,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Simple YAML parser for frontmatter
77
+ * Handles: strings, booleans, numbers, arrays, nested objects
78
+ */
79
+ function parseYaml(yamlStr: string): Record<string, unknown> {
80
+ const result: Record<string, unknown> = {};
81
+ const lines = yamlStr.split("\n");
82
+ let currentKey = "";
83
+ let currentIndent = 0;
84
+ const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [];
85
+
86
+ for (const line of lines) {
87
+ const trimmed = line.trim();
88
+ if (!trimmed || trimmed.startsWith("#")) continue;
89
+
90
+ const indent = line.search(/\S/);
91
+ const colonIdx = trimmed.indexOf(":");
92
+
93
+ if (colonIdx === -1) continue;
94
+
95
+ const key = trimmed.slice(0, colonIdx).trim();
96
+ let value: unknown = trimmed.slice(colonIdx + 1).trim();
97
+
98
+ // Pop stack while indent decreases
99
+ while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
100
+ stack.pop();
101
+ }
102
+
103
+ const parent = stack.length > 0 ? stack[stack.length - 1].obj : result;
104
+
105
+ // Parse value
106
+ if (value === "" || value === "null" || value === "~") {
107
+ value = null;
108
+ } else if (value === "true") {
109
+ value = true;
110
+ } else if (value === "false") {
111
+ value = false;
112
+ } else if (/^-?\d+(\.\d+)?$/.test(value as string)) {
113
+ value = parseFloat(value as string);
114
+ } else if ((value as string).startsWith("[") && (value as string).endsWith("]")) {
115
+ // Array
116
+ const arrContent = (value as string).slice(1, -1);
117
+ value = arrContent
118
+ ? arrContent.split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""))
119
+ : [];
120
+ } else {
121
+ // String - remove quotes
122
+ value = (value as string).replace(/^["']|["']$/g, "");
123
+ }
124
+
125
+ parent[key] = value;
126
+ currentKey = key;
127
+ currentIndent = indent;
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Serialize frontmatter to YAML string
135
+ */
136
+ export function serializeFrontmatter(
137
+ frontmatter: WikiPageFrontmatter
138
+ ): string {
139
+ const lines: string[] = [];
140
+
141
+ for (const [key, value] of Object.entries(frontmatter)) {
142
+ if (value === undefined || value === null) continue;
143
+
144
+ if (Array.isArray(value)) {
145
+ if (value.length === 0) {
146
+ lines.push(`${key}: []`);
147
+ } else if (value.every((v) => typeof v === "object")) {
148
+ // Array of objects
149
+ lines.push(`${key}:`);
150
+ for (const item of value) {
151
+ lines.push(` - ${JSON.stringify(item)}`);
152
+ }
153
+ } else {
154
+ lines.push(`${key}: [${value.join(", ")}]`);
155
+ }
156
+ } else if (typeof value === "object") {
157
+ // Nested object
158
+ lines.push(`${key}:`);
159
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
160
+ lines.push(` ${k}: ${JSON.stringify(v)}`);
161
+ }
162
+ } else if (typeof value === "boolean" || typeof value === "number") {
163
+ lines.push(`${key}: ${value}`);
164
+ } else {
165
+ // String - quote if needed
166
+ const str = String(value);
167
+ if (str.includes(":") || str.includes("#") || str.includes("\n")) {
168
+ lines.push(`${key}: "${str.replace(/"/g, '\\"')}"`);
169
+ } else {
170
+ lines.push(`${key}: ${str}`);
171
+ }
172
+ }
173
+ }
174
+
175
+ return lines.join("\n");
176
+ }
177
+
178
+ /**
179
+ * Combine frontmatter and body into full page content
180
+ */
181
+ export function combinePage(
182
+ frontmatter: WikiPageFrontmatter,
183
+ body: string
184
+ ): string {
185
+ return `---\n${serializeFrontmatter(frontmatter)}\n---\n\n${body}`;
186
+ }
187
+
188
+ // ============================================================================
189
+ // Storage Implementation
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Wiki storage manager
194
+ */
195
+ export class WikiStorage {
196
+ private config: WikiConfig;
197
+ private index: Map<string, PageIndexEntry> = new Map();
198
+ private operations: WikiOperation[] = [];
199
+ private stagedChanges: Map<string, StagedChange> = new Map();
200
+ private dirty = false;
201
+
202
+ constructor(config: WikiConfig) {
203
+ this.config = config;
204
+ this.ensureStructure();
205
+ this.loadIndex();
206
+ this.loadOperations();
207
+ this.loadStaged();
208
+ }
209
+
210
+ /**
211
+ * Get wiki configuration
212
+ */
213
+ getConfig(): WikiConfig {
214
+ return this.config;
215
+ }
216
+
217
+ /**
218
+ * Initialize wiki structure
219
+ */
220
+ initialize(): WikiInitResult {
221
+ const existing = existsSync(this.config.wikiRoot);
222
+ if (!existing) {
223
+ mkdirSync(this.config.wikiRoot, { recursive: true });
224
+ }
225
+
226
+ // Create subdirectories
227
+ for (const dir of [
228
+ this.config.rawDir,
229
+ this.config.knowledgeDir,
230
+ this.config.harnessDir,
231
+ ]) {
232
+ if (!existsSync(dir)) {
233
+ mkdirSync(dir, { recursive: true });
234
+ }
235
+ }
236
+
237
+ // Create schema file if not exists
238
+ if (!existsSync(this.config.schemaFile)) {
239
+ writeFileSync(
240
+ this.config.schemaFile,
241
+ `# Wiki Maintenance Conventions
242
+
243
+ ## Page Structure
244
+
245
+ Every wiki page must have:
246
+
247
+ \`\`\`yaml
248
+ ---
249
+ id: <uuid>
250
+ title: <human-readable title>
251
+ kind: <concept|decision|incident|plan|verification|reference>
252
+ projectId: <canonical project identifier>
253
+ status: <proposed|implemented|verified|stale|superseded>
254
+ createdAt: <ISO timestamp>
255
+ updatedAt: <ISO timestamp>
256
+ sources: []
257
+ relatedPages: []
258
+ ---
259
+ \`\`\`
260
+
261
+ ## Status Lifecycle
262
+
263
+ - \`proposed\`: Awaiting synthesis/verification
264
+ - \`implemented\`: Applied but not yet verified
265
+ - \`verified\`: Test/inspection evidence recorded
266
+ - \`stale\`: Source changed, needs review
267
+ - \`superseded\`: Replaced by another page
268
+
269
+ ## Source References
270
+
271
+ Always cite immutable snapshots, never live files:
272
+
273
+ \`\`\`yaml
274
+ sources:
275
+ - sourceId: <snapshot-id>
276
+ path: <original path>
277
+ contentHash: <sha256>
278
+ capturedAt: <ISO timestamp>
279
+ redacted: <true|false>
280
+ \`\`\`
281
+
282
+ ## Verification
283
+
284
+ A page is \`verified\` only with recorded evidence:
285
+
286
+ \`\`\`yaml
287
+ verification:
288
+ evidence: <test name or inspection reference>
289
+ scope: <what was verified>
290
+ version: <code version>
291
+ \`\`\`
292
+ `,
293
+ "utf8"
294
+ );
295
+ }
296
+
297
+ return {
298
+ created: !existing,
299
+ existing,
300
+ config: this.config,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Ensure directory structure exists
306
+ */
307
+ private ensureStructure(): void {
308
+ if (!existsSync(this.config.wikiRoot)) {
309
+ mkdirSync(this.config.wikiRoot, { recursive: true });
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Load page index from disk
315
+ */
316
+ private loadIndex(): void {
317
+ this.index.clear();
318
+ if (!existsSync(this.config.indexFile)) return;
319
+
320
+ try {
321
+ const content = readFileSync(this.config.indexFile, "utf8");
322
+ const entries: PageIndexEntry[] = JSON.parse(content);
323
+ for (const entry of entries) {
324
+ this.index.set(entry.id, entry);
325
+ }
326
+ } catch {
327
+ // Invalid index, rebuild
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Save page index to disk (atomic)
333
+ */
334
+ private saveIndex(): void {
335
+ const entries = Array.from(this.index.values());
336
+ const content = JSON.stringify(entries, null, 2);
337
+ const tmpFile = this.config.indexFile + ".tmp";
338
+ writeFileSync(tmpFile, content, "utf8");
339
+ renameSync(tmpFile, this.config.indexFile);
340
+ this.dirty = false;
341
+ }
342
+
343
+ /**
344
+ * Load operations from log
345
+ */
346
+ private loadOperations(): void {
347
+ this.operations = [];
348
+ if (!existsSync(this.config.logFile)) return;
349
+
350
+ try {
351
+ const content = readFileSync(this.config.logFile, "utf8");
352
+ const lines = content.split("\n").filter((l) => l.trim());
353
+ for (const line of lines) {
354
+ try {
355
+ this.operations.push(JSON.parse(line));
356
+ } catch {
357
+ // Skip invalid lines
358
+ }
359
+ }
360
+ } catch {
361
+ // No log yet
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Append operation to log
367
+ */
368
+ private appendOperation(op: WikiOperation): void {
369
+ const line = JSON.stringify(op) + "\n";
370
+ writeFileSync(this.config.logFile, line, { flag: "a", encoding: "utf8" });
371
+ this.operations.push(op);
372
+ }
373
+
374
+ /**
375
+ * Load staged changes
376
+ */
377
+ private loadStaged(): void {
378
+ this.stagedChanges.clear();
379
+ const stagedFile = join(this.config.harnessDir, "staged.json");
380
+ if (!existsSync(stagedFile)) return;
381
+
382
+ try {
383
+ const content = readFileSync(stagedFile, "utf8");
384
+ const changes: StagedChange[] = JSON.parse(content);
385
+ for (const change of changes) {
386
+ this.stagedChanges.set(change.pageId, change);
387
+ }
388
+ } catch {
389
+ // Invalid staged file
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Save staged changes (atomic)
395
+ */
396
+ private saveStaged(): void {
397
+ const changes = Array.from(this.stagedChanges.values());
398
+ const stagedFile = join(this.config.harnessDir, "staged.json");
399
+ const content = JSON.stringify(changes, null, 2);
400
+ const tmpFile = stagedFile + ".tmp";
401
+ writeFileSync(tmpFile, content, "utf8");
402
+ renameSync(tmpFile, stagedFile);
403
+ }
404
+
405
+ // ========================================================================
406
+ // Source Snapshot Operations
407
+ // ========================================================================
408
+
409
+ /**
410
+ * Create immutable source snapshot
411
+ */
412
+ createSnapshot(
413
+ originalPath: string,
414
+ content: string,
415
+ operationId: string
416
+ ): SourceSnapshot {
417
+ const id = `snap-${randomUUID().slice(0, 8)}`;
418
+ const snapshot: SourceSnapshot = {
419
+ id,
420
+ originalPath,
421
+ contentHash: computeHash(content),
422
+ capturedAt: now(),
423
+ content,
424
+ redacted: false,
425
+ operationId,
426
+ };
427
+
428
+ // Save snapshot file
429
+ const snapFile = join(this.config.rawDir, `${id}.json`);
430
+ writeFileSync(snapFile, JSON.stringify(snapshot, null, 2), "utf8");
431
+
432
+ return snapshot;
433
+ }
434
+
435
+ /**
436
+ * Get source snapshot by ID
437
+ */
438
+ getSnapshot(snapshotId: string): SourceSnapshot | null {
439
+ const snapFile = join(this.config.rawDir, `${snapshotId}.json`);
440
+ if (!existsSync(snapFile)) return null;
441
+
442
+ try {
443
+ const content = readFileSync(snapFile, "utf8");
444
+ return JSON.parse(content);
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
449
+
450
+ // ========================================================================
451
+ // Page Operations
452
+ // ========================================================================
453
+
454
+ /**
455
+ * Save a wiki page
456
+ */
457
+ savePage(page: WikiPage): void {
458
+ const content = combinePage(page.frontmatter, page.body);
459
+ const pageFile = join(this.config.knowledgeDir, `${page.frontmatter.id}.md`);
460
+ const tmpFile = pageFile + ".tmp";
461
+ writeFileSync(tmpFile, content, "utf8");
462
+ renameSync(tmpFile, pageFile);
463
+
464
+ // Update index
465
+ const entry: PageIndexEntry = {
466
+ id: page.frontmatter.id,
467
+ title: page.frontmatter.title,
468
+ kind: page.frontmatter.kind,
469
+ status: page.frontmatter.status,
470
+ tags: [], // Extracted from body
471
+ summary: page.body.slice(0, 200),
472
+ updatedAt: page.frontmatter.updatedAt,
473
+ codeVersion: page.frontmatter.codeVersion,
474
+ };
475
+ this.index.set(entry.id, entry);
476
+ this.saveIndex();
477
+ this.dirty = true;
478
+ }
479
+
480
+ /**
481
+ * Load a wiki page
482
+ */
483
+ loadPage(pageId: string): WikiPage | null {
484
+ const pageFile = join(this.config.knowledgeDir, `${pageId}.md`);
485
+ if (!existsSync(pageFile)) return null;
486
+
487
+ const content = readFileSync(pageFile, "utf8");
488
+ const { frontmatter, body, raw } = parseFrontmatter(content);
489
+
490
+ if (!frontmatter) return null;
491
+
492
+ return { frontmatter, body, rawFrontmatter: raw };
493
+ }
494
+
495
+ /**
496
+ * Delete a wiki page
497
+ */
498
+ deletePage(pageId: string): void {
499
+ const pageFile = join(this.config.knowledgeDir, `${pageId}.md`);
500
+ if (existsSync(pageFile)) {
501
+ unlinkSync(pageFile);
502
+ }
503
+ this.index.delete(pageId);
504
+ this.saveIndex();
505
+ this.stagedChanges.delete(pageId);
506
+ this.saveStaged();
507
+ }
508
+
509
+ /**
510
+ * Get all pages
511
+ */
512
+ getAllPages(): WikiPage[] {
513
+ const pages: WikiPage[] = [];
514
+ if (!existsSync(this.config.knowledgeDir)) return pages;
515
+
516
+ const files = readdirSync(this.config.knowledgeDir);
517
+ for (const file of files) {
518
+ if (!file.endsWith(".md")) continue;
519
+ const pageId = file.replace(".md", "");
520
+ const page = this.loadPage(pageId);
521
+ if (page) pages.push(page);
522
+ }
523
+
524
+ return pages;
525
+ }
526
+
527
+ // ========================================================================
528
+ // Staged Changes Operations
529
+ // ========================================================================
530
+
531
+ /**
532
+ * Stage a change for later application
533
+ */
534
+ stageChange(change: StagedChange): void {
535
+ this.stagedChanges.set(change.pageId, change);
536
+ this.saveStaged();
537
+ }
538
+
539
+ /**
540
+ * Get staged change for a page
541
+ */
542
+ getStagedChange(pageId: string): StagedChange | undefined {
543
+ return this.stagedChanges.get(pageId);
544
+ }
545
+
546
+ /**
547
+ * Clear staged change
548
+ */
549
+ clearStagedChange(pageId: string): void {
550
+ this.stagedChanges.delete(pageId);
551
+ this.saveStaged();
552
+ }
553
+
554
+ /**
555
+ * Get all staged changes
556
+ */
557
+ getAllStagedChanges(): StagedChange[] {
558
+ return Array.from(this.stagedChanges.values());
559
+ }
560
+
561
+ // ========================================================================
562
+ // Operation Logging
563
+ // ========================================================================
564
+
565
+ /**
566
+ * Log an operation
567
+ */
568
+ logOperation(
569
+ type: WikiOperationType,
570
+ details: Record<string, unknown>,
571
+ requestId?: string
572
+ ): WikiOperation {
573
+ const op: WikiOperation = {
574
+ id: `op-${randomUUID().slice(0, 8)}`,
575
+ type,
576
+ status: "pending",
577
+ startedAt: now(),
578
+ requestId,
579
+ details,
580
+ };
581
+ this.appendOperation(op);
582
+ return op;
583
+ }
584
+
585
+ /**
586
+ * Update operation status
587
+ */
588
+ completeOperation(opId: string, status: WikiOperation["status"], error?: string): void {
589
+ const opIndex = this.operations.findIndex((o) => o.id === opId);
590
+ if (opIndex === -1) return;
591
+
592
+ this.operations[opIndex] = {
593
+ ...this.operations[opIndex],
594
+ status,
595
+ completedAt: now(),
596
+ error,
597
+ };
598
+
599
+ // Rewrite log with updated operation
600
+ const tmpFile = this.config.logFile + ".tmp";
601
+ const content = this.operations.map((o) => JSON.stringify(o)).join("\n") + "\n";
602
+ writeFileSync(tmpFile, content, "utf8");
603
+ renameSync(tmpFile, this.config.logFile);
604
+ }
605
+
606
+ /**
607
+ * Get recent operations
608
+ */
609
+ getRecentOperations(limit = 50): WikiOperation[] {
610
+ return this.operations.slice(-limit);
611
+ }
612
+
613
+ // ========================================================================
614
+ // Search
615
+ // ========================================================================
616
+
617
+ /**
618
+ * Search wiki pages
619
+ */
620
+ search(query: string, filters?: WikiSearchFilters, limit = 10): WikiSearchResult[] {
621
+ const queryLower = query.toLowerCase();
622
+ const results: WikiSearchResult[] = [];
623
+
624
+ for (const [id, entry] of this.index) {
625
+ // Apply filters
626
+ if (filters?.kinds?.length && !filters.kinds.includes(entry.kind)) continue;
627
+ if (filters?.statuses?.length && !filters.statuses.includes(entry.status)) continue;
628
+ if (filters?.updatedAfter && entry.updatedAt < filters.updatedAfter) continue;
629
+ if (filters?.updatedBefore && entry.updatedAt > filters.updatedBefore) continue;
630
+
631
+ // Calculate relevance
632
+ let relevance = 0;
633
+ const matchedOn: WikiSearchResult["matchedOn"] = [];
634
+
635
+ if (entry.title.toLowerCase().includes(queryLower)) {
636
+ relevance += 0.5;
637
+ matchedOn.push("title");
638
+ }
639
+ if (entry.summary.toLowerCase().includes(queryLower)) {
640
+ relevance += 0.3;
641
+ matchedOn.push("body");
642
+ }
643
+ if (entry.tags.some((t) => t.toLowerCase().includes(queryLower))) {
644
+ relevance += 0.2;
645
+ matchedOn.push("tags");
646
+ }
647
+
648
+ if (relevance > 0) {
649
+ results.push({
650
+ page: entry,
651
+ relevance,
652
+ matchedOn,
653
+ snippets: [entry.summary],
654
+ });
655
+ }
656
+ }
657
+
658
+ // Sort by relevance and limit
659
+ results.sort((a, b) => b.relevance - a.relevance);
660
+ return results.slice(0, limit);
661
+ }
662
+
663
+ /**
664
+ * Get page index
665
+ */
666
+ getIndex(): PageIndexEntry[] {
667
+ return Array.from(this.index.values());
668
+ }
669
+ }
670
+
671
+ // ============================================================================
672
+ // Factory
673
+ // ============================================================================
674
+
675
+ /**
676
+ * Create wiki storage for a workspace
677
+ */
678
+ export function createWikiStorage(
679
+ workspaceRoot: string,
680
+ projectId: string
681
+ ): WikiStorage {
682
+ const wikiRoot = join(workspaceRoot, "wiki");
683
+
684
+ const config: WikiConfig = {
685
+ wikiRoot,
686
+ rawDir: join(wikiRoot, "raw"),
687
+ knowledgeDir: join(wikiRoot, "knowledge"),
688
+ harnessDir: join(wikiRoot, ".harness"),
689
+ indexFile: join(wikiRoot, ".harness", "index.json"),
690
+ logFile: join(wikiRoot, ".harness", "log.jsonl"),
691
+ schemaFile: join(wikiRoot, "SCHEMA.md"),
692
+ projectId,
693
+ };
694
+
695
+ return new WikiStorage(config);
696
+ }