pi-harness-runtime 1.1.327 → 1.1.328

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 (111) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/context-compiler/package.json +1 -1
  20. package/packages/context-discovery/package.json +1 -1
  21. package/packages/context-manager/package.json +1 -1
  22. package/packages/cookie-sanitizer/package.json +1 -1
  23. package/packages/cost-optimizer/package.json +1 -1
  24. package/packages/dependency-analyzer/package.json +1 -1
  25. package/packages/django-plugin/package.json +1 -1
  26. package/packages/doc-generator/package.json +1 -1
  27. package/packages/durable-review/package.json +1 -1
  28. package/packages/e2e-tester/package.json +1 -1
  29. package/packages/evaluation-engine/package.json +1 -1
  30. package/packages/evaluation-runner/package.json +1 -1
  31. package/packages/event-bus/package.json +1 -1
  32. package/packages/event-store/package.json +1 -1
  33. package/packages/experience-replay/package.json +1 -1
  34. package/packages/feedback-collector/package.json +1 -1
  35. package/packages/file-copy-helper/package.json +1 -1
  36. package/packages/framework-detector/package.json +1 -1
  37. package/packages/framework-plugin-sdk/package.json +1 -1
  38. package/packages/frappe-plugin/package.json +1 -1
  39. package/packages/generic-web-plugin/package.json +1 -1
  40. package/packages/health-monitor/package.json +1 -1
  41. package/packages/intent-analyzer/package.json +1 -1
  42. package/packages/jev-judge/package.json +1 -1
  43. package/packages/knowledge-graph/package.json +1 -1
  44. package/packages/knowledge-retrieval/package.json +1 -1
  45. package/packages/laravel-plugin/package.json +1 -1
  46. package/packages/learning-engine/package.json +1 -1
  47. package/packages/mcp-adapter/package.json +1 -1
  48. package/packages/memory-engine/package.json +1 -1
  49. package/packages/milestone-manager/package.json +1 -1
  50. package/packages/model-registry/package.json +1 -1
  51. package/packages/nextjs-plugin/package.json +1 -1
  52. package/packages/notification/package.json +1 -1
  53. package/packages/observability/package.json +1 -1
  54. package/packages/okf-indexer/package.json +1 -1
  55. package/packages/paperclip-integration/package.json +1 -1
  56. package/packages/performance-optimizer/package.json +1 -1
  57. package/packages/privilege-broker/package.json +1 -1
  58. package/packages/project-analyzer/package.json +1 -1
  59. package/packages/project-bootstrap/package.json +1 -1
  60. package/packages/projection-engine/package.json +1 -1
  61. package/packages/prompt-compiler/package.json +1 -1
  62. package/packages/prompt-versioning/package.json +1 -1
  63. package/packages/provider-adapter-sdk/package.json +1 -1
  64. package/packages/provider-router/package.json +1 -1
  65. package/packages/provider-selector/package.json +1 -1
  66. package/packages/providers/package.json +1 -1
  67. package/packages/qdrant-skills/package.json +1 -1
  68. package/packages/quota-manager/package.json +1 -1
  69. package/packages/rate-limiter/package.json +1 -1
  70. package/packages/react-vite-plugin/package.json +1 -1
  71. package/packages/release-manager/package.json +1 -1
  72. package/packages/requirement-compiler/package.json +1 -1
  73. package/packages/runtime/package.json +1 -1
  74. package/packages/scheduler/package.json +1 -1
  75. package/packages/scheduler-adapter/package.json +1 -1
  76. package/packages/session/package.json +1 -1
  77. package/packages/session-api/package.json +1 -1
  78. package/packages/session-export/package.json +1 -1
  79. package/packages/shared-context/package.json +1 -1
  80. package/packages/skill-mcp-client/package.json +1 -1
  81. package/packages/skill-registry/package.json +1 -1
  82. package/packages/skills/package.json +1 -1
  83. package/packages/sprint-planner/package.json +1 -1
  84. package/packages/subscription-engine/package.json +1 -1
  85. package/packages/task-compiler/package.json +1 -1
  86. package/packages/telegram/package.json +1 -1
  87. package/packages/tencentdb-memory/package.json +1 -1
  88. package/packages/tencentdb-sync/package.json +1 -1
  89. package/packages/test-data-generator/package.json +1 -1
  90. package/packages/test-generator/package.json +1 -1
  91. package/packages/todo-bd-sync/package.json +1 -1
  92. package/packages/token-estimation/package.json +1 -1
  93. package/packages/token-optimizer/package.json +1 -1
  94. package/packages/trajectory/package.json +1 -1
  95. package/packages/tui/package.json +1 -1
  96. package/packages/types/package.json +1 -1
  97. package/packages/webmcp-tester/package.json +1 -1
  98. package/packages/wiki/dist/index.d.ts.map +1 -1
  99. package/packages/wiki/dist/index.js +35 -20
  100. package/packages/wiki/dist/index.js.map +1 -1
  101. package/packages/wiki/dist/storage.d.ts +27 -5
  102. package/packages/wiki/dist/storage.d.ts.map +1 -1
  103. package/packages/wiki/dist/storage.js +149 -39
  104. package/packages/wiki/dist/storage.js.map +1 -1
  105. package/packages/wiki/package.json +1 -1
  106. package/packages/wiki/src/index.ts +41 -23
  107. package/packages/wiki/src/storage.ts +164 -36
  108. package/packages/workflow-events/package.json +1 -1
  109. package/packages/workspace-scanner/package.json +1 -1
  110. package/packages/worktree/package.json +1 -1
  111. package/packages/write-review/package.json +1 -1
@@ -14,7 +14,7 @@ import {
14
14
  readdirSync,
15
15
  statSync,
16
16
  } from "node:fs";
17
- import { join, dirname, relative } from "node:path";
17
+ import { join, dirname, relative, resolve } from "node:path";
18
18
  import { createHash, randomUUID } from "node:crypto";
19
19
  import type {
20
20
  WikiConfig,
@@ -76,24 +76,75 @@ export function parseFrontmatter(
76
76
 
77
77
  /**
78
78
  * Simple YAML parser for frontmatter
79
- * Handles: strings, booleans, numbers, arrays, nested objects
79
+ * Handles: strings, booleans, numbers, arrays, nested objects, YAML list items
80
80
  */
81
81
  function parseYaml(yamlStr: string): Record<string, unknown> {
82
82
  const result: Record<string, unknown> = {};
83
83
  const lines = yamlStr.split("\n");
84
84
  const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [];
85
85
 
86
- for (const line of lines) {
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const line = lines[i];
87
88
  const trimmed = line.trim();
88
89
  if (!trimmed || trimmed.startsWith("#")) continue;
89
90
 
90
91
  const indent = line.search(/\S/);
91
- const colonIdx = trimmed.indexOf(":");
92
92
 
93
+ // Handle YAML list items (lines starting with '-')
94
+ if (trimmed.startsWith("-")) {
95
+ const itemContent = trimmed.slice(1).trim();
96
+ const colonIdx = itemContent.indexOf(":");
97
+
98
+ if (colonIdx !== -1) {
99
+ // This is a list item with key-value pairs on same line or next lines
100
+ const key = itemContent.slice(0, colonIdx).trim();
101
+ const value = itemContent.slice(colonIdx + 1).trim();
102
+
103
+ // Pop stack while indent decreases
104
+ while (stack.length > 0 && indent < stack[stack.length - 1].indent) {
105
+ stack.pop();
106
+ }
107
+
108
+ // Find or create the array
109
+ const parent = stack.length > 0 ? stack[stack.length - 1].obj : result;
110
+ const arrayKey = parent ? Object.keys(parent).slice(-1)[0] : undefined;
111
+ let array: unknown[];
112
+
113
+ if (arrayKey && Array.isArray(parent[arrayKey])) {
114
+ array = parent[arrayKey] as unknown[];
115
+ } else {
116
+ // Start a new array
117
+ const newArray: unknown[] = [];
118
+ if (parent) {
119
+ parent[arrayKey || key] = newArray;
120
+ } else {
121
+ result[key] = newArray;
122
+ }
123
+ array = newArray;
124
+ }
125
+
126
+ // Create new object for this list item
127
+ const item: Record<string, unknown> = {};
128
+ array.push(item);
129
+
130
+ // Parse the key-value on this line
131
+ const parsedValue = parseValue(value);
132
+ if (parsedValue !== undefined) {
133
+ item[key] = parsedValue;
134
+ }
135
+
136
+ // Push this item to stack for nested properties
137
+ stack.push({ obj: item, indent });
138
+ }
139
+ continue;
140
+ }
141
+
142
+ // Regular key-value line
143
+ const colonIdx = trimmed.indexOf(":");
93
144
  if (colonIdx === -1) continue;
94
145
 
95
146
  const key = trimmed.slice(0, colonIdx).trim();
96
- let value: unknown = trimmed.slice(colonIdx + 1).trim();
147
+ const rawValue = trimmed.slice(colonIdx + 1).trim();
97
148
 
98
149
  // Pop stack while indent decreases
99
150
  while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
@@ -102,32 +153,41 @@ function parseYaml(yamlStr: string): Record<string, unknown> {
102
153
 
103
154
  const parent = stack.length > 0 ? stack[stack.length - 1].obj : result;
104
155
 
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, "");
156
+ // Check if value is empty (nested content follows)
157
+ if (rawValue === "" || rawValue === "~" || rawValue === "null") {
158
+ // Nested content follows - create placeholder
159
+ const nested: Record<string, unknown> = {};
160
+ parent[key] = nested;
161
+ stack.push({ obj: nested, indent });
162
+ continue;
123
163
  }
124
164
 
125
- parent[key] = value;
165
+ parent[key] = parseValue(rawValue);
126
166
  }
127
167
 
128
168
  return result;
129
169
  }
130
170
 
171
+ /**
172
+ * Parse a YAML scalar value or array
173
+ */
174
+ type YamlValue = string | number | boolean | null | string[];
175
+ function parseValue(value: string): YamlValue {
176
+ if (value === "" || value === "null" || value === "~") {
177
+ return null;
178
+ }
179
+ if (value === "true") return true;
180
+ if (value === "false") return false;
181
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
182
+ return parseFloat(value);
183
+ }
184
+ if (value.startsWith("[") && value.endsWith("]")) {
185
+ const content = value.slice(1, -1);
186
+ return content ? content.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")) : [];
187
+ }
188
+ return value.replace(/^["']|["']$/g, "");
189
+ }
190
+
131
191
  /**
132
192
  * Serialize frontmatter to YAML string
133
193
  */
@@ -200,13 +260,35 @@ export class WikiStorage {
200
260
  private indexMtime = 0; // Track when index was last loaded
201
261
  private operations: WikiOperation[] = [];
202
262
  private stagedChanges: Map<string, StagedChange> = new Map();
263
+ private initialized = false;
203
264
 
204
265
  constructor(config: WikiConfig) {
205
266
  this.config = config;
206
- this.ensureStructure();
267
+ // Don't create directories until explicitly initialized
268
+ // This prevents writes when the feature is disabled
269
+ }
270
+
271
+ /**
272
+ * Initialize storage (call before any read/write operations)
273
+ * This is called automatically by WikiManager when feature is enabled
274
+ */
275
+ /**
276
+ * Prepare storage for use (load indexes and operations)
277
+ * Called after directories are ensured by initialize()
278
+ */
279
+ private prepareStorage(): void {
280
+ if (this.initialized) return;
207
281
  this.loadIndex();
208
282
  this.loadOperations();
209
283
  this.loadStaged();
284
+ this.initialized = true;
285
+ }
286
+
287
+ /**
288
+ * Check if storage is initialized
289
+ */
290
+ isInitialized(): boolean {
291
+ return this.initialized;
210
292
  }
211
293
 
212
294
  /**
@@ -303,15 +385,6 @@ verification:
303
385
  };
304
386
  }
305
387
 
306
- /**
307
- * Ensure directory structure exists
308
- */
309
- private ensureStructure(): void {
310
- if (!existsSync(this.config.wikiRoot)) {
311
- mkdirSync(this.config.wikiRoot, { recursive: true });
312
- }
313
- }
314
-
315
388
  /**
316
389
  * Load page index from disk (if changed since last load)
317
390
  */
@@ -349,16 +422,44 @@ verification:
349
422
  }
350
423
 
351
424
  /**
352
- * Save page index to disk (atomic)
425
+ * Save page index to disk (atomic, merges with existing entries)
353
426
  */
354
427
  private saveIndex(): void {
355
- const entries = Array.from(this.index.values());
428
+ // Reload from disk to merge (handles concurrent writes)
429
+ const diskEntries = this.loadIndexFromDisk();
430
+ const merged = new Map<string, PageIndexEntry>(diskEntries);
431
+
432
+ // Apply in-memory changes
433
+ for (const [id, entry] of this.index) {
434
+ merged.set(id, entry);
435
+ }
436
+
437
+ const entries = Array.from(merged.values());
356
438
  const content = JSON.stringify(entries, null, 2);
357
439
  const tmpFile = this.config.indexFile + ".tmp";
358
440
  writeFileSync(tmpFile, content, "utf8");
359
441
  renameSync(tmpFile, this.config.indexFile);
360
442
  }
361
443
 
444
+ /**
445
+ * Load index from disk only (no cache)
446
+ */
447
+ private loadIndexFromDisk(): Map<string, PageIndexEntry> {
448
+ const result = new Map<string, PageIndexEntry>();
449
+ if (!existsSync(this.config.indexFile)) return result;
450
+
451
+ try {
452
+ const content = readFileSync(this.config.indexFile, "utf8");
453
+ const entries: PageIndexEntry[] = JSON.parse(content);
454
+ for (const entry of entries) {
455
+ result.set(entry.id, entry);
456
+ }
457
+ } catch {
458
+ // Invalid index, return empty
459
+ }
460
+ return result;
461
+ }
462
+
362
463
  /**
363
464
  * Load operations from log
364
465
  */
@@ -455,7 +556,9 @@ verification:
455
556
  * Get source snapshot by ID
456
557
  */
457
558
  getSnapshot(snapshotId: string): SourceSnapshot | null {
559
+ this.validateSnapshotId(snapshotId);
458
560
  const snapFile = join(this.config.rawDir, `${snapshotId}.json`);
561
+ this.ensureWithinDirectory(snapFile, this.config.rawDir);
459
562
  if (!existsSync(snapFile)) return null;
460
563
 
461
564
  try {
@@ -474,8 +577,10 @@ verification:
474
577
  * Save a wiki page
475
578
  */
476
579
  savePage(page: WikiPage): void {
580
+ this.validatePageId(page.frontmatter.id);
477
581
  const content = combinePage(page.frontmatter, page.body);
478
582
  const pageFile = join(this.config.knowledgeDir, `${page.frontmatter.id}.md`);
583
+ this.ensureWithinDirectory(pageFile, this.config.knowledgeDir);
479
584
  const tmpFile = pageFile + ".tmp";
480
585
  writeFileSync(tmpFile, content, "utf8");
481
586
  renameSync(tmpFile, pageFile);
@@ -499,7 +604,9 @@ verification:
499
604
  * Load a wiki page
500
605
  */
501
606
  loadPage(pageId: string): WikiPage | null {
607
+ this.validatePageId(pageId);
502
608
  const pageFile = join(this.config.knowledgeDir, `${pageId}.md`);
609
+ this.ensureWithinDirectory(pageFile, this.config.knowledgeDir);
503
610
  if (!existsSync(pageFile)) return null;
504
611
 
505
612
  const content = readFileSync(pageFile, "utf8");
@@ -520,6 +627,27 @@ verification:
520
627
  }
521
628
  }
522
629
 
630
+ /**
631
+ * Validate snapshot ID to prevent path traversal attacks
632
+ */
633
+ private validateSnapshotId(snapshotId: string): void {
634
+ // Reject paths with directory separators or parent references
635
+ if (snapshotId.includes("/") || snapshotId.includes("\\") || snapshotId.includes("..")) {
636
+ throw new Error(`Invalid snapshot ID: ${snapshotId}`);
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Ensure a path is within the expected directory (defense in depth)
642
+ */
643
+ private ensureWithinDirectory(filePath: string, expectedDir: string): void {
644
+ const resolved = resolve(filePath);
645
+ const expected = resolve(expectedDir);
646
+ if (!resolved.startsWith(expected + "/") && resolved !== expected) {
647
+ throw new Error(`Path ${filePath} is outside expected directory ${expectedDir}`);
648
+ }
649
+ }
650
+
523
651
  /**
524
652
  * Delete a wiki page
525
653
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/workflow-events",
3
- "version": "1.1.327",
3
+ "version": "1.1.328",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/workspace-scanner",
3
- "version": "1.1.327",
3
+ "version": "1.1.328",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/worktree",
3
- "version": "1.1.327",
3
+ "version": "1.1.328",
4
4
  "description": "pi-harness-runtime — worktree",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/write-review",
3
- "version": "1.1.327",
3
+ "version": "1.1.328",
4
4
  "description": "Two-agent write-review loop with blackboard coordination",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",