pi-harness-runtime 1.1.410 → 1.1.411

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 (146) 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/autonomous-runtime/test/types.test.ts +59 -32
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
  27. package/packages/cost-optimizer/package.json +1 -1
  28. package/packages/dependency-analyzer/dist/src/imports.js +3 -3
  29. package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
  30. package/packages/dependency-analyzer/package.json +1 -1
  31. package/packages/dependency-analyzer/src/imports.ts +3 -3
  32. package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
  33. package/packages/django-plugin/package.json +1 -1
  34. package/packages/doc-generator/package.json +1 -1
  35. package/packages/durable-review/package.json +1 -1
  36. package/packages/durable-review/test/acceptance.test.ts +24 -2
  37. package/packages/durable-review/test/smoke.test.ts +85 -47
  38. package/packages/e2e-tester/package.json +1 -1
  39. package/packages/evaluation-engine/package.json +1 -1
  40. package/packages/evaluation-runner/package.json +1 -1
  41. package/packages/event-bus/package.json +1 -1
  42. package/packages/event-bus/test/bus.test.ts +16 -4
  43. package/packages/event-sink/package.json +1 -1
  44. package/packages/event-store/package.json +1 -1
  45. package/packages/experience-replay/package.json +1 -1
  46. package/packages/feedback-collector/package.json +1 -1
  47. package/packages/file-copy-helper/package.json +1 -1
  48. package/packages/framework-detector/package.json +1 -1
  49. package/packages/framework-plugin-sdk/package.json +1 -1
  50. package/packages/frappe-plugin/package.json +1 -1
  51. package/packages/generic-web-plugin/package.json +1 -1
  52. package/packages/health-monitor/package.json +1 -1
  53. package/packages/intent-analyzer/package.json +1 -1
  54. package/packages/jev-judge/package.json +1 -1
  55. package/packages/knowledge-graph/package.json +1 -1
  56. package/packages/knowledge-retrieval/package.json +1 -1
  57. package/packages/laravel-plugin/package.json +1 -1
  58. package/packages/learning-engine/package.json +1 -1
  59. package/packages/logging/package.json +1 -1
  60. package/packages/mcp-adapter/package.json +1 -1
  61. package/packages/memory-engine/package.json +1 -1
  62. package/packages/milestone-manager/package.json +1 -1
  63. package/packages/model-registry/package.json +1 -1
  64. package/packages/nextjs-plugin/package.json +1 -1
  65. package/packages/notification/package.json +1 -1
  66. package/packages/observability/package.json +1 -1
  67. package/packages/okf-indexer/package.json +1 -1
  68. package/packages/paperclip-integration/package.json +1 -1
  69. package/packages/performance-optimizer/package.json +1 -1
  70. package/packages/permission-mode/package.json +1 -1
  71. package/packages/privilege-broker/package.json +1 -1
  72. package/packages/project-analyzer/package.json +1 -1
  73. package/packages/project-bootstrap/package.json +1 -1
  74. package/packages/projection-engine/package.json +1 -1
  75. package/packages/prompt-compiler/package.json +1 -1
  76. package/packages/prompt-versioning/package.json +1 -1
  77. package/packages/provider-adapter-sdk/package.json +1 -1
  78. package/packages/provider-router/package.json +1 -1
  79. package/packages/provider-selector/package.json +1 -1
  80. package/packages/providers/package.json +1 -1
  81. package/packages/qdrant-skills/dist/embedder.d.ts +27 -0
  82. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  83. package/packages/qdrant-skills/dist/embedder.js +54 -0
  84. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  85. package/packages/qdrant-skills/dist/index.d.ts +1 -1
  86. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  87. package/packages/qdrant-skills/dist/index.js +1 -1
  88. package/packages/qdrant-skills/dist/index.js.map +1 -1
  89. package/packages/qdrant-skills/package.json +1 -1
  90. package/packages/qdrant-skills/src/embedder.ts +90 -0
  91. package/packages/qdrant-skills/src/index.ts +3 -0
  92. package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
  93. package/packages/quota-manager/package.json +1 -1
  94. package/packages/rate-limiter/package.json +1 -1
  95. package/packages/react-vite-plugin/package.json +1 -1
  96. package/packages/relay/package.json +1 -1
  97. package/packages/release-manager/package.json +1 -1
  98. package/packages/requirement-compiler/package.json +1 -1
  99. package/packages/runtime/package.json +1 -1
  100. package/packages/scheduler/package.json +1 -1
  101. package/packages/scheduler-adapter/package.json +1 -1
  102. package/packages/session/package.json +1 -1
  103. package/packages/session-api/package.json +1 -1
  104. package/packages/session-export/package.json +1 -1
  105. package/packages/shared-context/package.json +1 -1
  106. package/packages/skill-mcp-client/package.json +1 -1
  107. package/packages/skill-registry/package.json +1 -1
  108. package/packages/skills/dist/index.d.ts +5 -0
  109. package/packages/skills/dist/index.d.ts.map +1 -1
  110. package/packages/skills/dist/index.js +4 -1
  111. package/packages/skills/dist/index.js.map +1 -1
  112. package/packages/skills/dist/loader.d.ts +94 -3
  113. package/packages/skills/dist/loader.d.ts.map +1 -1
  114. package/packages/skills/dist/loader.js +192 -25
  115. package/packages/skills/dist/loader.js.map +1 -1
  116. package/packages/skills/package.json +1 -1
  117. package/packages/skills/src/index.ts +12 -3
  118. package/packages/skills/src/loader.ts +243 -27
  119. package/packages/skills/test/init-skills.test.ts +107 -111
  120. package/packages/skills/test/vector-indexing.test.ts +205 -0
  121. package/packages/sprint-planner/package.json +1 -1
  122. package/packages/subscription-engine/package.json +1 -1
  123. package/packages/task-compiler/package.json +1 -1
  124. package/packages/telegram/package.json +1 -1
  125. package/packages/tencentdb-memory/package.json +1 -1
  126. package/packages/tencentdb-sync/package.json +1 -1
  127. package/packages/test-data-generator/package.json +1 -1
  128. package/packages/test-generator/package.json +1 -1
  129. package/packages/todo-bd-sync/package.json +1 -1
  130. package/packages/token-estimation/package.json +1 -1
  131. package/packages/token-optimizer/package.json +1 -1
  132. package/packages/tooling-preference/package.json +1 -1
  133. package/packages/trajectory/package.json +1 -1
  134. package/packages/tui/package.json +1 -1
  135. package/packages/two-phase-review/package.json +1 -1
  136. package/packages/types/package.json +1 -1
  137. package/packages/voice-api/package.json +1 -1
  138. package/packages/webmcp-tester/package.json +1 -1
  139. package/packages/wiki/package.json +1 -1
  140. package/packages/workflow-events/package.json +1 -1
  141. package/packages/workspace-scanner/package.json +1 -1
  142. package/packages/workspace-scanner/test/scanner.test.ts +4 -3
  143. package/packages/worktree/package.json +1 -1
  144. package/packages/write-review/package.json +1 -1
  145. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
  146. package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
@@ -23,7 +23,7 @@ import { parseSkillFile, scanSkillDirectory } from "./parser.js";
23
23
  // Loaded dynamically so skills work without Qdrant configured.
24
24
  // At runtime, packages/ is resolved from the monorepo root or node_modules.
25
25
 
26
- interface QdrantSkillPoint {
26
+ export interface QdrantSkillPoint {
27
27
  id: number;
28
28
  name: string;
29
29
  description: string;
@@ -43,13 +43,62 @@ interface QdrantClientLike {
43
43
 
44
44
  interface QdrantEmbedderLike {
45
45
  createEmbedding(text: string, apiKey?: string): Promise<{ embedding: number[]; tokens: number }>;
46
+ /** One request for many texts. Absent in an older package build. */
47
+ createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
46
48
  }
47
49
 
50
+ /**
51
+ * A point of the vector index: the vector and the payload of one skill.
52
+ */
53
+ export interface SkillVectorPoint {
54
+ id: number;
55
+ vector: number[];
56
+ payload: QdrantSkillPoint;
57
+ }
58
+
59
+ /**
60
+ * The vector index of a registry.
61
+ *
62
+ * The index is an interface so a caller can supply its own implementation
63
+ * (a test uses a counting double) and so the registry never has to know
64
+ * whether the index lives in Qdrant.
65
+ */
66
+ export interface SkillVectorIndexer {
67
+ /** One vector per text, in the order of the texts. */
68
+ embed(texts: string[]): Promise<number[][]>;
69
+ /** Store the points of the given skills. */
70
+ upsert(points: SkillVectorPoint[]): Promise<void>;
71
+ }
72
+
73
+ /**
74
+ * The options of a skill registry.
75
+ */
76
+ export interface SkillRegistryOptions {
77
+ /**
78
+ * The vector index.
79
+ *
80
+ * `undefined` (the default) indexes into the configured Qdrant cluster.
81
+ * `null` disables the vector index entirely. An object is used as the index.
82
+ */
83
+ indexer?: SkillVectorIndexer | null;
84
+ /**
85
+ * How many skills one indexing request carries. A larger batch costs fewer
86
+ * requests but holds more of them in memory.
87
+ */
88
+ batchSize?: number;
89
+ }
90
+
91
+ /**
92
+ * The default number of skills one indexing request carries.
93
+ */
94
+ const DEFAULT_INDEX_BATCH_SIZE = 32;
95
+
48
96
  /** Resolve the Qdrant skill module path (monorepo sibling or npm package) */
49
97
  async function resolveQdrantModules(): Promise<{
50
98
  createQdrantClient: (cfg: unknown) => { upsert(c: string, o: unknown): Promise<void>; query(c: string, o: unknown): Promise<unknown>; };
51
99
  createCollection: (client: unknown, name: string, dims: number) => Promise<void>;
52
100
  createEmbedding: (text: string, apiKey?: string) => Promise<{ embedding: number[]; tokens: number }>;
101
+ createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
53
102
  } | null> {
54
103
  // Try the monorepo sibling first (the compiled package, so the module
55
104
  // comes from the same build the app ships).
@@ -60,6 +109,7 @@ async function resolveQdrantModules(): Promise<{
60
109
  createQdrantClient: unknown;
61
110
  createCollection: unknown;
62
111
  createEmbedding: unknown;
112
+ createEmbeddingsBatch?: unknown;
63
113
  };
64
114
  return mod as Awaited<ReturnType<typeof resolveQdrantModules>>;
65
115
  } catch {
@@ -71,11 +121,21 @@ async function resolveQdrantModules(): Promise<{
71
121
  // The specifier is a variable on purpose: the package is optional and
72
122
  // must not be resolved by the type checker.
73
123
  const specifier = "@pi-harness/qdrant-skills";
74
- const mod = (await import(/* @vite-ignore */ specifier)) as { createQdrantClient: unknown; createCollection: unknown; createEmbedding: unknown };
124
+ const mod = (await import(/* @vite-ignore */ specifier)) as {
125
+ createQdrantClient: unknown;
126
+ createCollection: unknown;
127
+ createEmbedding: unknown;
128
+ createEmbeddingsBatch?: unknown;
129
+ };
75
130
  return {
76
131
  createQdrantClient: (cfg: unknown) => (mod.createQdrantClient as Function)(cfg) as { upsert(c: string, o: unknown): Promise<void>; query(c: string, o: unknown): Promise<unknown> },
77
132
  createCollection: mod.createCollection as (client: unknown, name: string, dims: number) => Promise<void>,
78
133
  createEmbedding: mod.createEmbedding as (text: string, apiKey?: string) => Promise<{ embedding: number[]; tokens: number }>,
134
+ // Absent in an older package build: the registry falls back to one
135
+ // request per text, still with a bounded concurrency.
136
+ createEmbeddingsBatch: typeof mod.createEmbeddingsBatch === "function"
137
+ ? (mod.createEmbeddingsBatch as (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>)
138
+ : undefined,
79
139
  };
80
140
  } catch {
81
141
  return null;
@@ -128,6 +188,16 @@ export class SkillRegistry {
128
188
  private index: SkillIndex = { entries: [], loadedAt: 0 };
129
189
  private loading: Map<string, Promise<Skill>> = new Map();
130
190
 
191
+ // The vector index. `undefined` means "use Qdrant", `null` means "no index".
192
+ private _indexer: SkillVectorIndexer | null | undefined;
193
+ private _batchSize: number;
194
+ // The skills that still have to be indexed, in registration order.
195
+ private _indexQueue: Array<{ skill: Skill; point: QdrantSkillPoint; hash: number }> = [];
196
+ private _indexFlushRunning = false;
197
+ private _indexFlushScheduled = false;
198
+ // The content hash of the version of a skill that was queued or indexed.
199
+ private _indexedHashes: Map<string, number> = new Map();
200
+
131
201
  // Qdrant state (lazily initialized on first register if configured)
132
202
  private _qdrant: {
133
203
  client: QdrantClientLike;
@@ -137,6 +207,12 @@ export class SkillRegistry {
137
207
  } | null = null;
138
208
  private _qdrantInitPromise: Promise<void> | null = null;
139
209
  private _qdrantInitFailed = false;
210
+ private _qdrantIndexer: SkillVectorIndexer | null = null;
211
+
212
+ constructor(options: SkillRegistryOptions = {}) {
213
+ this._indexer = options.indexer;
214
+ this._batchSize = options.batchSize ?? DEFAULT_INDEX_BATCH_SIZE;
215
+ }
140
216
 
141
217
  /**
142
218
  * Check if Qdrant initialization has failed (useful for diagnostics).
@@ -145,6 +221,13 @@ export class SkillRegistry {
145
221
  return this._qdrantInitFailed;
146
222
  }
147
223
 
224
+ /**
225
+ * Whether skills are still queued for the vector index.
226
+ */
227
+ isIndexing(): boolean {
228
+ return this._indexFlushRunning || this._indexQueue.length > 0;
229
+ }
230
+
148
231
  /**
149
232
  * Initialize Qdrant client if configured.
150
233
  * Uses a shared init promise to prevent race conditions when multiple
@@ -181,7 +264,10 @@ export class SkillRegistry {
181
264
  await mods.createCollection(client, config.collection, 1536);
182
265
  this._qdrant = {
183
266
  client,
184
- embedder: { createEmbedding: mods.createEmbedding },
267
+ embedder: {
268
+ createEmbedding: mods.createEmbedding,
269
+ createEmbeddingsBatch: mods.createEmbeddingsBatch,
270
+ },
185
271
  collection: config.collection,
186
272
  ready: true,
187
273
  };
@@ -191,28 +277,141 @@ export class SkillRegistry {
191
277
  }
192
278
  }
193
279
 
194
- /**
195
- * Index a single skill into Qdrant (non-blocking).
196
- */
197
- private async _indexSkillToQdrant(skill: Skill): Promise<void> {
198
- if (!this._qdrant?.ready) return;
199
-
200
- const point: QdrantSkillPoint = {
280
+ /** The point of a skill in the vector index. */
281
+ private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
282
+ return {
201
283
  id: this._hashId(skill.id),
202
284
  name: skill.id,
203
285
  description: skill.frontmatter.description,
204
286
  body: skill.body.slice(0, 2000),
205
287
  text: [skill.id, skill.frontmatter.name, skill.frontmatter.description, skill.body.slice(0, 1500)].join(" "),
206
288
  };
289
+ }
207
290
 
208
- try {
209
- const { embedding } = await this._qdrant.embedder.createEmbedding(point.text);
210
- await this._qdrant.client.upsert(this._qdrant.collection, {
211
- wait: false,
212
- points: [{ id: point.id, vector: embedding, payload: point }],
213
- });
214
- } catch {
215
- // Silently skip — embeddings are optional
291
+ /**
292
+ * The indexer of the registry: the injected one, or the Qdrant cluster.
293
+ */
294
+ private _getIndexer(): SkillVectorIndexer | null {
295
+ if (this._indexer !== undefined) return this._indexer;
296
+ if (!this._qdrantIndexer) {
297
+ this._qdrantIndexer = this._createQdrantIndexer();
298
+ }
299
+ return this._qdrantIndexer;
300
+ }
301
+
302
+ /**
303
+ * The Qdrant cluster as an indexer.
304
+ *
305
+ * The batch call of the embeddings endpoint carries many texts in one
306
+ * request. One request per skill exhausted the sockets of the process when
307
+ * a registry held hundreds of skills, and every other request of the
308
+ * program then starved.
309
+ */
310
+ private _createQdrantIndexer(): SkillVectorIndexer | null {
311
+ const qdrant = this._qdrant;
312
+ if (!qdrant?.ready) return null;
313
+
314
+ return {
315
+ embed: async (texts: string[]): Promise<number[][]> => {
316
+ if (texts.length === 0) return [];
317
+ const batch = qdrant.embedder.createEmbeddingsBatch;
318
+ if (batch) {
319
+ const result = await batch(texts);
320
+ return result.embeddings;
321
+ }
322
+ // An older build without the batch call: one request per text, but
323
+ // the caller keeps the concurrency bounded all the same.
324
+ const embeddings: number[][] = [];
325
+ for (const text of texts) {
326
+ const { embedding } = await qdrant.embedder.createEmbedding(text);
327
+ embeddings.push(embedding);
328
+ }
329
+ return embeddings;
330
+ },
331
+ upsert: async (points: SkillVectorPoint[]): Promise<void> => {
332
+ await qdrant.client.upsert(qdrant.collection, { wait: false, points });
333
+ },
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Queue a skill for the vector index.
339
+ *
340
+ * A skill whose text did not change is not queued again: the registry is
341
+ * filled on every scan, and re-embedding the same skill would pay for the
342
+ * same vector over and over.
343
+ */
344
+ private _scheduleIndex(skill: Skill): void {
345
+ const point = this._toQdrantPoint(skill);
346
+ const hash = this._hashId(point.text);
347
+ if (this._indexedHashes.get(skill.id) === hash) return;
348
+
349
+ this._indexedHashes.set(skill.id, hash);
350
+ this._indexQueue.push({ skill, point, hash });
351
+ this._scheduleIndexFlush();
352
+ }
353
+
354
+ /**
355
+ * Start the index flush, a microtask from now.
356
+ *
357
+ * A scan registers every skill in one go. Waiting for the microtask lets
358
+ * that burst share one request instead of starting one request per skill.
359
+ */
360
+ private _scheduleIndexFlush(): void {
361
+ if (this._indexFlushScheduled) return;
362
+ this._indexFlushScheduled = true;
363
+ queueMicrotask(() => {
364
+ this._indexFlushScheduled = false;
365
+ this._runIndexFlush();
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Index the queued skills, one batch at a time.
371
+ *
372
+ * Only one batch is in flight, so the number of open connections stays
373
+ * bound no matter how many skills the registry holds.
374
+ */
375
+ private _runIndexFlush(): void {
376
+ if (this._indexFlushRunning) return;
377
+ this._indexFlushRunning = true;
378
+
379
+ void this._flushIndexQueue().finally(() => {
380
+ this._indexFlushRunning = false;
381
+ // Skills may have arrived while the flush was running.
382
+ if (this._indexQueue.length > 0) this._scheduleIndexFlush();
383
+ });
384
+ }
385
+
386
+ private async _flushIndexQueue(): Promise<void> {
387
+ if (this._indexer === undefined) await this._ensureQdrant();
388
+ const indexer = this._getIndexer();
389
+ if (!indexer) {
390
+ // No vector index: the skills stay usable without vectors.
391
+ this._indexQueue = [];
392
+ return;
393
+ }
394
+
395
+ while (this._indexQueue.length > 0) {
396
+ const batch = this._indexQueue.splice(0, this._batchSize);
397
+ try {
398
+ const vectors = await indexer.embed(batch.map((item) => item.point.text));
399
+ const points: SkillVectorPoint[] = [];
400
+ batch.forEach((item, position) => {
401
+ const vector = vectors[position];
402
+ if (!vector || vector.length === 0) return;
403
+ points.push({ id: item.point.id, vector, payload: item.point });
404
+ });
405
+ if (points.length > 0) await indexer.upsert(points);
406
+ } catch {
407
+ // The vector index is optional. A failed batch may be retried by a
408
+ // later registration of the same skills.
409
+ for (const item of batch) {
410
+ if (this._indexedHashes.get(item.skill.id) === item.hash) {
411
+ this._indexedHashes.delete(item.skill.id);
412
+ }
413
+ }
414
+ }
216
415
  }
217
416
  }
218
417
 
@@ -231,8 +430,8 @@ export class SkillRegistry {
231
430
  register(skill: Skill): void {
232
431
  this.skills.set(skill.id, skill);
233
432
  this.updateIndex(skill);
234
- // Trigger async Qdrant indexing without blocking registration
235
- this._ensureQdrant().then(() => this._indexSkillToQdrant(skill)).catch(() => {});
433
+ // Queue the skill for the vector index without blocking registration.
434
+ this._scheduleIndex(skill);
236
435
  }
237
436
 
238
437
  /**
@@ -413,13 +612,30 @@ export class SkillRegistry {
413
612
  * Useful after Qdrant becomes available or collection is rebuilt.
414
613
  */
415
614
  async syncAllToQdrant(onProgress?: (index: number, total: number) => void): Promise<void> {
416
- await this._ensureQdrant();
417
- if (!this._qdrant?.ready) return;
615
+ if (this._indexer === undefined) await this._ensureQdrant();
616
+ const indexer = this._getIndexer();
617
+ if (!indexer) return;
418
618
 
419
619
  const skills = this.list();
420
- for (let i = 0; i < skills.length; i++) {
421
- await this._indexSkillToQdrant(skills[i]);
422
- onProgress?.(i + 1, skills.length);
620
+ const total = skills.length;
621
+ let done = 0;
622
+ for (let i = 0; i < total; i += this._batchSize) {
623
+ const batch = skills.slice(i, i + this._batchSize);
624
+ const points = batch.map((skill) => this._toQdrantPoint(skill));
625
+ try {
626
+ const vectors = await indexer.embed(points.map((point) => point.text));
627
+ const payload: SkillVectorPoint[] = [];
628
+ points.forEach((point, position) => {
629
+ const vector = vectors[position];
630
+ if (!vector || vector.length === 0) return;
631
+ payload.push({ id: point.id, vector, payload: point });
632
+ });
633
+ if (payload.length > 0) await indexer.upsert(payload);
634
+ } catch {
635
+ // The vector index is optional — a failed batch does not stop the sync.
636
+ }
637
+ done += batch.length;
638
+ onProgress?.(done, total);
423
639
  }
424
640
  }
425
641
 
@@ -576,8 +792,8 @@ export class SkillRegistry {
576
792
  /**
577
793
  * Create a new skill registry
578
794
  */
579
- export function createSkillRegistry(): SkillRegistry {
580
- return new SkillRegistry();
795
+ export function createSkillRegistry(options: SkillRegistryOptions = {}): SkillRegistry {
796
+ return new SkillRegistry(options);
581
797
  }
582
798
 
583
799
  /**
@@ -1,118 +1,114 @@
1
1
  /**
2
- * Skills Initialization E2E Test
3
- *
4
- * Tests the initSkills function to ensure:
5
- * 1. initSkills() with no args uses default directories
6
- * 2. initSkills({ locations }) with explicit locations works
7
- * 3. initSkills({ locations: undefined }) falls back to defaults (the bug case)
8
- *
9
- * Bug: Previously, passing undefined for locations would override defaults
10
- * with undefined, causing "directories is not iterable" error.
11
- *
12
- * Usage:
13
- * bun packages/skills/test/init-skills.test.ts
2
+ * The initialization of the skills system.
3
+ *
4
+ * The test used to be a script that ran `initSkills()` while the module was
5
+ * imported. That scanned the real skill directories of the machine and
6
+ * started the vector indexing of every skill it found, which opened a
7
+ * connection per skill and starved every other test of the process.
8
+ *
9
+ * The test now uses a temporary directory and a registry without a vector
10
+ * index, so it neither reads the machine nor reaches the network.
14
11
  */
15
12
 
13
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
14
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
16
18
  import { initSkills } from "../src/index.js";
19
+ import { createSkillRegistry } from "../src/loader.js";
20
+
21
+ let tempDir: string;
17
22
 
18
- async function runTests() {
19
- console.log("=== initSkills E2E Test ===\n");
20
-
21
- let passed = 0;
22
- let failed = 0;
23
-
24
- // Test 1: no args
25
- console.log("[Test 1] initSkills() with no arguments...");
26
- try {
27
- const result = initSkills();
28
- if (!result || !Array.isArray(result.skills) || !Array.isArray(result.errors)) {
29
- throw new Error("Invalid result structure");
30
- }
31
- console.log(` PASS: Returned ${result.skills.length} skills, ${result.errors.length} errors\n`);
32
- passed++;
33
- } catch (error) {
34
- console.error(` FAIL: ${error}\n`);
35
- failed++;
36
- }
37
-
38
- // Test 2: explicit locations
39
- console.log("[Test 2] initSkills({ locations: [...] })...");
40
- try {
41
- const result = initSkills({ locations: ["/tmp/test"] });
42
- if (!result) {
43
- throw new Error("No result returned");
44
- }
45
- console.log(` PASS: Returned ${result.skills.length} skills\n`);
46
- passed++;
47
- } catch (error) {
48
- console.error(` FAIL: ${error}\n`);
49
- failed++;
50
- }
51
-
52
- // Test 3: undefined locations (the bug case)
53
- console.log("[Test 3] initSkills({ locations: undefined }) - THE BUG CASE...");
54
- try {
55
- const result = initSkills({ locations: undefined });
56
- if (!result || !Array.isArray(result.skills)) {
57
- throw new Error("Invalid result structure");
58
- }
59
- console.log(` PASS: Returned ${result.skills.length} skills\n`);
60
- passed++;
61
- } catch (error) {
62
- console.error(` FAIL: ${error}\n`);
63
- failed++;
64
- }
65
-
66
- // Test 4: partial config with only maxDepth
67
- console.log("[Test 4] initSkills({ maxDepth: 5 })...");
68
- try {
69
- const result = initSkills({ maxDepth: 5 });
70
- if (!result || !Array.isArray(result.skills)) {
71
- throw new Error("Invalid result structure");
72
- }
73
- console.log(` PASS: Returned ${result.skills.length} skills\n`);
74
- passed++;
75
- } catch (error) {
76
- console.error(` FAIL: ${error}\n`);
77
- failed++;
78
- }
79
-
80
- // Test 5: partial config with only includeHidden
81
- console.log("[Test 5] initSkills({ includeHidden: true })...");
82
- try {
83
- const result = initSkills({ includeHidden: true });
84
- if (!result || !Array.isArray(result.skills)) {
85
- throw new Error("Invalid result structure");
86
- }
87
- console.log(` PASS: Returned ${result.skills.length} skills\n`);
88
- passed++;
89
- } catch (error) {
90
- console.error(` FAIL: ${error}\n`);
91
- failed++;
92
- }
93
-
94
- // Test 6: empty locations array
95
- console.log("[Test 6] initSkills({ locations: [] })...");
96
- try {
97
- const result = initSkills({ locations: [] });
98
- if (!result) {
99
- throw new Error("No result returned");
100
- }
101
- console.log(` PASS: Returned ${result.skills.length} skills\n`);
102
- passed++;
103
- } catch (error) {
104
- console.error(` FAIL: ${error}\n`);
105
- failed++;
106
- }
107
-
108
- // Summary
109
- console.log("=== Summary ===");
110
- console.log(`Passed: ${passed}`);
111
- console.log(`Failed: ${failed}`);
112
-
113
- if (failed > 0) {
114
- process.exit(1);
115
- }
23
+ /** Write a skill into a directory of the temporary home. */
24
+ function writeSkill(directory: string, name: string): void {
25
+ const skillDir = join(tempDir, directory, name);
26
+ mkdirSync(skillDir, { recursive: true });
27
+ writeFileSync(
28
+ join(skillDir, "SKILL.md"),
29
+ [
30
+ "---",
31
+ `name: ${name}`,
32
+ `description: The ${name} skill.`,
33
+ "version: 1.0.0",
34
+ "---",
35
+ "",
36
+ `The body of the ${name} skill.`,
37
+ "",
38
+ ].join("\n"),
39
+ "utf8",
40
+ );
116
41
  }
117
42
 
118
- runTests();
43
+ beforeEach(() => {
44
+ tempDir = mkdtempSync(join(tmpdir(), "test-init-skills-"));
45
+ mkdirSync(join(tempDir, "skills"), { recursive: true });
46
+ writeSkill("skills", "alpha");
47
+ writeSkill("skills", "beta");
48
+ });
49
+
50
+ afterEach(() => {
51
+ rmSync(tempDir, { recursive: true, force: true });
52
+ });
53
+
54
+ describe("the initialization of the skills system", () => {
55
+ it("scans the given locations", () => {
56
+ const result = initSkills({
57
+ locations: [join(tempDir, "skills")],
58
+ registry: createSkillRegistry({ indexer: null }),
59
+ });
60
+
61
+ expect(result.skills.length).toBe(2);
62
+ expect(result.skills.map((skill) => skill.id).sort()).toEqual(["alpha", "beta"]);
63
+ expect(result.errors).toEqual([]);
64
+ });
65
+
66
+ it("falls back to the default locations when none are given", () => {
67
+ // The default locations point at the machine of the test, so the
68
+ // result is only checked for its shape.
69
+ const result = initSkills({
70
+ registry: createSkillRegistry({ indexer: null }),
71
+ });
72
+
73
+ expect(Array.isArray(result.skills)).toBe(true);
74
+ expect(Array.isArray(result.errors)).toBe(true);
75
+ });
76
+
77
+ it("falls back to the default locations when they are undefined", () => {
78
+ // The bug case: an undefined value must not override the defaults.
79
+ const result = initSkills({
80
+ locations: undefined,
81
+ registry: createSkillRegistry({ indexer: null }),
82
+ });
83
+
84
+ expect(Array.isArray(result.skills)).toBe(true);
85
+ expect(Array.isArray(result.errors)).toBe(true);
86
+ });
87
+
88
+ it("accepts a partial configuration", () => {
89
+ const result = initSkills({
90
+ maxDepth: 5,
91
+ includeHidden: true,
92
+ registry: createSkillRegistry({ indexer: null }),
93
+ });
94
+
95
+ expect(Array.isArray(result.skills)).toBe(true);
96
+ });
97
+
98
+ it("accepts an empty list of locations", () => {
99
+ const result = initSkills({
100
+ locations: [],
101
+ registry: createSkillRegistry({ indexer: null }),
102
+ });
103
+
104
+ expect(result.skills).toEqual([]);
105
+ });
106
+
107
+ it("registers the scanned skills in the registry", () => {
108
+ const registry = createSkillRegistry({ indexer: null });
109
+
110
+ initSkills({ locations: [join(tempDir, "skills")], registry });
111
+
112
+ expect(registry.list().length).toBe(2);
113
+ });
114
+ });