pi-harness-runtime 1.1.410 → 1.1.412

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 (171) hide show
  1. package/harness/notification-settings.ts +130 -0
  2. package/harness/skill-remote-source.ts +157 -0
  3. package/harness/skill-sync.ts +74 -22
  4. package/harness/skills-startup.ts +202 -0
  5. package/harness/startup-report.ts +98 -56
  6. package/harness-startup-status.ts +31 -2
  7. package/index.ts +125 -31
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/agent-policy/package.json +1 -1
  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/autonomous-runtime/test/types.test.ts +59 -32
  16. package/packages/cache-strategy/package.json +1 -1
  17. package/packages/capability-registry/package.json +1 -1
  18. package/packages/checkpoint/package.json +1 -1
  19. package/packages/cli-plugin-sdk/package.json +1 -1
  20. package/packages/clipboard/package.json +1 -1
  21. package/packages/clipboard-plugin/package.json +1 -1
  22. package/packages/code-generation/package.json +1 -1
  23. package/packages/code-review/package.json +1 -1
  24. package/packages/codex-adapter/package.json +1 -1
  25. package/packages/codex-watcher/package.json +1 -1
  26. package/packages/config-capture/package.json +1 -1
  27. package/packages/connection-runtime/package.json +1 -1
  28. package/packages/context-compiler/package.json +1 -1
  29. package/packages/context-discovery/package.json +1 -1
  30. package/packages/context-manager/package.json +1 -1
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/cookie-sanitizer/package.json +1 -1
  33. package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
  34. package/packages/cost-optimizer/package.json +1 -1
  35. package/packages/dependency-analyzer/dist/src/imports.js +3 -3
  36. package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
  37. package/packages/dependency-analyzer/package.json +1 -1
  38. package/packages/dependency-analyzer/src/imports.ts +3 -3
  39. package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
  40. package/packages/django-plugin/package.json +1 -1
  41. package/packages/doc-generator/package.json +1 -1
  42. package/packages/durable-review/package.json +1 -1
  43. package/packages/durable-review/test/acceptance.test.ts +24 -2
  44. package/packages/durable-review/test/smoke.test.ts +85 -47
  45. package/packages/e2e-tester/package.json +1 -1
  46. package/packages/evaluation-engine/package.json +1 -1
  47. package/packages/evaluation-runner/package.json +1 -1
  48. package/packages/event-bus/package.json +1 -1
  49. package/packages/event-bus/test/bus.test.ts +16 -4
  50. package/packages/event-sink/package.json +1 -1
  51. package/packages/event-store/package.json +1 -1
  52. package/packages/experience-replay/package.json +1 -1
  53. package/packages/feedback-collector/package.json +1 -1
  54. package/packages/file-copy-helper/package.json +1 -1
  55. package/packages/framework-detector/package.json +1 -1
  56. package/packages/framework-plugin-sdk/package.json +1 -1
  57. package/packages/frappe-plugin/package.json +1 -1
  58. package/packages/generic-web-plugin/package.json +1 -1
  59. package/packages/health-monitor/package.json +1 -1
  60. package/packages/intent-analyzer/package.json +1 -1
  61. package/packages/jev-judge/package.json +1 -1
  62. package/packages/knowledge-graph/package.json +1 -1
  63. package/packages/knowledge-retrieval/package.json +1 -1
  64. package/packages/laravel-plugin/package.json +1 -1
  65. package/packages/learning-engine/package.json +1 -1
  66. package/packages/logging/package.json +1 -1
  67. package/packages/mcp-adapter/package.json +1 -1
  68. package/packages/memory-engine/package.json +1 -1
  69. package/packages/milestone-manager/package.json +1 -1
  70. package/packages/model-registry/package.json +1 -1
  71. package/packages/nextjs-plugin/package.json +1 -1
  72. package/packages/notification/package.json +1 -1
  73. package/packages/observability/package.json +1 -1
  74. package/packages/okf-indexer/package.json +1 -1
  75. package/packages/paperclip-integration/package.json +1 -1
  76. package/packages/performance-optimizer/package.json +1 -1
  77. package/packages/permission-mode/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  89. package/packages/qdrant-skills/dist/client.js +21 -8
  90. package/packages/qdrant-skills/dist/client.js.map +1 -1
  91. package/packages/qdrant-skills/dist/embedder.d.ts +37 -0
  92. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  93. package/packages/qdrant-skills/dist/embedder.js +65 -1
  94. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  95. package/packages/qdrant-skills/dist/index.d.ts +1 -1
  96. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  97. package/packages/qdrant-skills/dist/index.js +1 -1
  98. package/packages/qdrant-skills/dist/index.js.map +1 -1
  99. package/packages/qdrant-skills/package.json +1 -1
  100. package/packages/qdrant-skills/src/client.ts +19 -8
  101. package/packages/qdrant-skills/src/embedder.ts +103 -1
  102. package/packages/qdrant-skills/src/index.ts +5 -0
  103. package/packages/qdrant-skills/test/collection-info.test.ts +129 -0
  104. package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
  105. package/packages/quota-manager/package.json +1 -1
  106. package/packages/rate-limiter/package.json +1 -1
  107. package/packages/react-vite-plugin/package.json +1 -1
  108. package/packages/relay/package.json +1 -1
  109. package/packages/release-manager/package.json +1 -1
  110. package/packages/requirement-compiler/package.json +1 -1
  111. package/packages/runtime/package.json +1 -1
  112. package/packages/scheduler/package.json +1 -1
  113. package/packages/scheduler-adapter/package.json +1 -1
  114. package/packages/session/package.json +1 -1
  115. package/packages/session-api/package.json +1 -1
  116. package/packages/session-export/package.json +1 -1
  117. package/packages/shared-context/package.json +1 -1
  118. package/packages/skill-mcp-client/package.json +1 -1
  119. package/packages/skill-registry/package.json +1 -1
  120. package/packages/skills/dist/index.d.ts +19 -1
  121. package/packages/skills/dist/index.d.ts.map +1 -1
  122. package/packages/skills/dist/index.js +7 -2
  123. package/packages/skills/dist/index.js.map +1 -1
  124. package/packages/skills/dist/loader.d.ts +273 -3
  125. package/packages/skills/dist/loader.d.ts.map +1 -1
  126. package/packages/skills/dist/loader.js +525 -36
  127. package/packages/skills/dist/loader.js.map +1 -1
  128. package/packages/skills/dist/remote-source.d.ts +107 -0
  129. package/packages/skills/dist/remote-source.d.ts.map +1 -0
  130. package/packages/skills/dist/remote-source.js +79 -0
  131. package/packages/skills/dist/remote-source.js.map +1 -0
  132. package/packages/skills/dist/vector-schema.d.ts +58 -0
  133. package/packages/skills/dist/vector-schema.d.ts.map +1 -0
  134. package/packages/skills/dist/vector-schema.js +55 -0
  135. package/packages/skills/dist/vector-schema.js.map +1 -0
  136. package/packages/skills/package.json +1 -1
  137. package/packages/skills/src/index.ts +28 -5
  138. package/packages/skills/src/loader.ts +688 -53
  139. package/packages/skills/src/remote-source.ts +148 -0
  140. package/packages/skills/src/vector-schema.ts +72 -0
  141. package/packages/skills/test/init-skills.test.ts +107 -111
  142. package/packages/skills/test/remote-skills.test.ts +242 -0
  143. package/packages/skills/test/remote-source.test.ts +248 -0
  144. package/packages/skills/test/vector-indexing.test.ts +205 -0
  145. package/packages/sprint-planner/package.json +1 -1
  146. package/packages/subscription-engine/package.json +1 -1
  147. package/packages/task-compiler/package.json +1 -1
  148. package/packages/telegram/package.json +1 -1
  149. package/packages/tencentdb-memory/package.json +1 -1
  150. package/packages/tencentdb-sync/package.json +1 -1
  151. package/packages/test-data-generator/package.json +1 -1
  152. package/packages/test-generator/package.json +1 -1
  153. package/packages/todo-bd-sync/package.json +1 -1
  154. package/packages/token-estimation/package.json +1 -1
  155. package/packages/token-optimizer/package.json +1 -1
  156. package/packages/tooling-preference/package.json +1 -1
  157. package/packages/trajectory/package.json +1 -1
  158. package/packages/tui/package.json +1 -1
  159. package/packages/two-phase-review/package.json +1 -1
  160. package/packages/types/package.json +1 -1
  161. package/packages/voice-api/package.json +1 -1
  162. package/packages/webmcp-tester/package.json +1 -1
  163. package/packages/wiki/package.json +1 -1
  164. package/packages/workflow-events/package.json +1 -1
  165. package/packages/workspace-scanner/package.json +1 -1
  166. package/packages/workspace-scanner/test/scanner.test.ts +4 -3
  167. package/packages/worktree/package.json +1 -1
  168. package/packages/write-review/package.json +1 -1
  169. package/scripts/sync-to-qdrant.ts +79 -14
  170. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
  171. package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The skills that live on the server.
3
+ *
4
+ * The vector store is an index - it says WHICH skill fits a question. The
5
+ * service holds the authoritative files, so a machine without a local copy
6
+ * can still use a skill.
7
+ *
8
+ * The states must stay apart: a service that refused the call needs a
9
+ * different fix than a service that could not be reached, and both differ
10
+ * from a service that holds nothing.
11
+ *
12
+ * The source is injected, so the test neither reaches the network nor
13
+ * replaces a process-global module.
14
+ */
15
+
16
+ import { describe, expect, it } from "bun:test";
17
+
18
+ import { createSkillRegistry } from "../src/loader.js";
19
+ import type { RemoteSkill, RemoteSkillSource } from "../src/remote-source.js";
20
+
21
+ /** A source that holds the given skills. */
22
+ class FakeSource implements RemoteSkillSource {
23
+ /** The skills the source holds. */
24
+ readonly skills: RemoteSkill[];
25
+ /** The ids that were asked for. */
26
+ readonly requested: string[] = [];
27
+ /** The error the next call raises. */
28
+ failure: { message: string; code: "denied" | "unavailable" } | null = null;
29
+
30
+ constructor(skills: RemoteSkill[] = []) {
31
+ this.skills = skills;
32
+ }
33
+
34
+ async list(): Promise<RemoteSkill[]> {
35
+ if (this.failure) {
36
+ const error = new Error(this.failure.message) as Error & {
37
+ code: string;
38
+ };
39
+ error.code = this.failure.code;
40
+ throw error;
41
+ }
42
+ return this.skills;
43
+ }
44
+
45
+ async get(id: string): Promise<RemoteSkill | null> {
46
+ this.requested.push(id);
47
+ if (this.failure) {
48
+ const error = new Error(this.failure.message) as Error & {
49
+ code: string;
50
+ };
51
+ error.code = this.failure.code;
52
+ throw error;
53
+ }
54
+ return this.skills.find((skill) => skill.id === id) ?? null;
55
+ }
56
+ }
57
+
58
+ describe("the skills of the server", () => {
59
+ it("are listed by the registry", async () => {
60
+ const source = new FakeSource([
61
+ {
62
+ id: "shared-skill",
63
+ name: "shared-skill",
64
+ description: "the shared skill",
65
+ version: "1.2.0",
66
+ },
67
+ ]);
68
+ const registry = createSkillRegistry();
69
+ registry.setRemoteSource(source);
70
+
71
+ const stats = await registry.refreshRemoteSkills();
72
+
73
+ expect(stats.state).toBe("populated");
74
+ expect(stats.count).toBe(1);
75
+ expect(registry.listRemote().map((skill) => skill.id)).toEqual(["shared-skill"]);
76
+ // The local count stays local.
77
+ expect(registry.list()).toHaveLength(0);
78
+ });
79
+
80
+ it("are not local skills", async () => {
81
+ const source = new FakeSource([
82
+ { id: "shared-skill", name: "shared-skill", description: "d", version: "1.0.0" },
83
+ ]);
84
+ const registry = createSkillRegistry();
85
+ registry.setRemoteSource(source);
86
+ await registry.refreshRemoteSkills();
87
+
88
+ expect(registry.list()).toHaveLength(0);
89
+ expect(registry.listRemote()).toHaveLength(1);
90
+ });
91
+
92
+ it("replace the cached skill when the version changed", async () => {
93
+ const source = new FakeSource([
94
+ { id: "shared-skill", name: "shared-skill", description: "old", version: "1.0.0" },
95
+ ]);
96
+ const registry = createSkillRegistry();
97
+ registry.setRemoteSource(source);
98
+ await registry.refreshRemoteSkills();
99
+ expect(registry.listRemote()[0].frontmatter.description).toBe("old");
100
+
101
+ // The server publishes a new version.
102
+ source.skills[0] = {
103
+ id: "shared-skill",
104
+ name: "shared-skill",
105
+ description: "new",
106
+ version: "1.1.0",
107
+ };
108
+ await registry.refreshRemoteSkills();
109
+
110
+ expect(registry.listRemote()[0].frontmatter.description).toBe("new");
111
+ expect(registry.listRemote()[0].frontmatter.version).toBe("1.1.0");
112
+ });
113
+
114
+ it("stay when the version is unknown", async () => {
115
+ const source = new FakeSource([
116
+ { id: "shared-skill", name: "shared-skill", description: "known", version: "1.0.0" },
117
+ ]);
118
+ const registry = createSkillRegistry();
119
+ registry.setRemoteSource(source);
120
+ await registry.refreshRemoteSkills();
121
+
122
+ // A record without a version is not evidence of a change.
123
+ source.skills[0] = {
124
+ id: "shared-skill",
125
+ name: "shared-skill",
126
+ description: "changed without a version",
127
+ };
128
+ await registry.refreshRemoteSkills();
129
+
130
+ expect(registry.listRemote()[0].frontmatter.description).toBe("known");
131
+ });
132
+
133
+ it("are loaded completely from the service", async () => {
134
+ const source = new FakeSource([
135
+ {
136
+ id: "shared-skill",
137
+ name: "shared-skill",
138
+ description: "the shared skill",
139
+ version: "1.0.0",
140
+ content: "the complete body of the skill",
141
+ contentComplete: true,
142
+ },
143
+ ]);
144
+ const registry = createSkillRegistry();
145
+ registry.setRemoteSource(source);
146
+
147
+ const loaded = await registry.load("shared-skill");
148
+
149
+ expect(loaded).not.toBeNull();
150
+ expect(loaded?.body).toBe("the complete body of the skill");
151
+ expect(source.requested).toContain("shared-skill");
152
+ });
153
+
154
+ it("are not asked for again once the content is complete", async () => {
155
+ const source = new FakeSource([
156
+ {
157
+ id: "shared-skill",
158
+ name: "shared-skill",
159
+ description: "d",
160
+ version: "1.0.0",
161
+ content: "the body",
162
+ contentComplete: true,
163
+ },
164
+ ]);
165
+ const registry = createSkillRegistry();
166
+ registry.setRemoteSource(source);
167
+
168
+ await registry.load("shared-skill");
169
+ const afterFirst = source.requested.length;
170
+ await registry.load("shared-skill");
171
+
172
+ expect(source.requested.length).toBe(afterFirst);
173
+ });
174
+
175
+ it("are reported as empty when the service holds nothing", async () => {
176
+ const registry = createSkillRegistry();
177
+ registry.setRemoteSource(new FakeSource([]));
178
+
179
+ const stats = await registry.refreshRemoteSkills();
180
+
181
+ expect(stats.state).toBe("empty");
182
+ expect(stats.count).toBe(0);
183
+ });
184
+
185
+ it("are reported as not configured without a service", async () => {
186
+ const registry = createSkillRegistry();
187
+
188
+ const stats = await registry.getRemoteStats();
189
+
190
+ expect(stats.state).toBe("not-configured");
191
+ });
192
+ });
193
+
194
+ describe("a service that does not answer", () => {
195
+ it("reports a refusal as denied", async () => {
196
+ const source = new FakeSource();
197
+ source.failure = { message: "unauthorized", code: "denied" };
198
+ const registry = createSkillRegistry();
199
+ registry.setRemoteSource(source);
200
+
201
+ const stats = await registry.refreshRemoteSkills();
202
+
203
+ expect(stats.state).toBe("denied");
204
+ expect(stats.error).toContain("unauthorized");
205
+ });
206
+
207
+ it("reports an unreachable service as unavailable", async () => {
208
+ const source = new FakeSource();
209
+ source.failure = { message: "connection refused", code: "unavailable" };
210
+ const registry = createSkillRegistry();
211
+ registry.setRemoteSource(source);
212
+
213
+ const stats = await registry.refreshRemoteSkills();
214
+
215
+ expect(stats.state).toBe("unavailable");
216
+ expect(stats.error).toContain("connection refused");
217
+ });
218
+
219
+ it("keeps the cached skills after a failed refresh", async () => {
220
+ const source = new FakeSource([
221
+ { id: "shared-skill", name: "shared-skill", description: "d", version: "1.0.0" },
222
+ ]);
223
+ const registry = createSkillRegistry();
224
+ registry.setRemoteSource(source);
225
+ await registry.refreshRemoteSkills();
226
+
227
+ source.failure = { message: "connection refused", code: "unavailable" };
228
+ const stats = await registry.refreshRemoteSkills();
229
+
230
+ expect(stats.state).toBe("unavailable");
231
+ // A failed refresh must not throw the cached skills away.
232
+ expect(registry.listRemote()).toHaveLength(1);
233
+ });
234
+
235
+ it("does not put a credential into the message", async () => {
236
+ const source = new FakeSource();
237
+ source.failure = {
238
+ message: "Bearer sk-abcdefghijklmnopqrstuvwxyz0123456789 refused",
239
+ code: "denied",
240
+ };
241
+ const registry = createSkillRegistry();
242
+ registry.setRemoteSource(source);
243
+
244
+ const stats = await registry.refreshRemoteSkills();
245
+
246
+ expect(stats.error).not.toContain("sk-abcdefghijklmnopqrstuvwxyz0123456789");
247
+ });
248
+ });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Regression tests for the vector indexing of the skill registry.
3
+ *
4
+ * The registry used to start one embedding request per registered skill, all
5
+ * at the same time. A registry with a few hundred skills opened a few hundred
6
+ * connections at once, which starved every other request of the process.
7
+ *
8
+ * These tests use an injected indexer, so they neither reach the network nor
9
+ * touch a process-global module.
10
+ */
11
+
12
+ import { describe, expect, test } from "bun:test";
13
+
14
+ import {
15
+ createSkillRegistry,
16
+ type SkillVectorIndexer,
17
+ type SkillVectorPoint,
18
+ } from "../src/loader.js";
19
+ import type { Skill } from "../src/types.js";
20
+
21
+ /** An indexer that counts the requests it receives. */
22
+ class CountingIndexer implements SkillVectorIndexer {
23
+ /** The number of texts of every embed call. */
24
+ readonly embedCalls: string[][] = [];
25
+ /** The points of every upsert call. */
26
+ readonly upsertCalls: SkillVectorPoint[][] = [];
27
+ /** The number of embed calls that were in flight at the same time. */
28
+ maxConcurrentEmbeds = 0;
29
+ private inFlight = 0;
30
+ /** When set, every embed call waits for this gate. */
31
+ gate: { promise: Promise<void>; release: () => void } | null = null;
32
+
33
+ async embed(texts: string[]): Promise<number[][]> {
34
+ this.inFlight += 1;
35
+ this.maxConcurrentEmbeds = Math.max(this.maxConcurrentEmbeds, this.inFlight);
36
+ this.embedCalls.push(texts);
37
+ try {
38
+ if (this.gate) await this.gate.promise;
39
+ return texts.map((text) => [text.length, 1, 2]);
40
+ } finally {
41
+ this.inFlight -= 1;
42
+ }
43
+ }
44
+
45
+ async upsert(points: SkillVectorPoint[]): Promise<void> {
46
+ this.upsertCalls.push(points);
47
+ }
48
+
49
+ /** The number of texts that were embedded. */
50
+ get embeddedCount(): number {
51
+ return this.embedCalls.reduce((total, texts) => total + texts.length, 0);
52
+ }
53
+ }
54
+
55
+ /** A skill with the given id and body. */
56
+ function makeSkill(id: string, body = `body of ${id}`): Skill {
57
+ return {
58
+ id,
59
+ path: `/tmp/skills/${id}`,
60
+ frontmatter: {
61
+ name: id,
62
+ description: `description of ${id}`,
63
+ version: "1.0.0",
64
+ },
65
+ body,
66
+ references: [],
67
+ examples: [],
68
+ metadata: {},
69
+ loaded: false,
70
+ };
71
+ }
72
+
73
+ /** Wait until the registry has no queued skill left. */
74
+ async function settle(registry: { isIndexing(): boolean }): Promise<void> {
75
+ for (let attempt = 0; attempt < 200 && registry.isIndexing(); attempt++) {
76
+ await new Promise((resolve) => setTimeout(resolve, 5));
77
+ }
78
+ }
79
+
80
+ describe("skill vector indexing", () => {
81
+ test("one embedding request carries many skills", async () => {
82
+ const indexer = new CountingIndexer();
83
+ const registry = createSkillRegistry({ indexer });
84
+
85
+ for (let i = 0; i < 100; i++) {
86
+ registry.register(makeSkill(`skill-${i}`));
87
+ }
88
+ await settle(registry);
89
+
90
+ expect(indexer.embeddedCount).toBe(100);
91
+ expect(indexer.embedCalls.length).toBeLessThanOrEqual(4);
92
+ expect(indexer.maxConcurrentEmbeds).toBe(1);
93
+ expect(indexer.upsertCalls.length).toBe(indexer.embedCalls.length);
94
+ });
95
+
96
+ test("the number of requests does not grow with the number of skills", async () => {
97
+ const small = new CountingIndexer();
98
+ const smallRegistry = createSkillRegistry({ indexer: small, batchSize: 8 });
99
+ for (let i = 0; i < 8; i++) smallRegistry.register(makeSkill(`small-${i}`));
100
+ await settle(smallRegistry);
101
+
102
+ const large = new CountingIndexer();
103
+ const largeRegistry = createSkillRegistry({ indexer: large, batchSize: 8 });
104
+ for (let i = 0; i < 80; i++) largeRegistry.register(makeSkill(`large-${i}`));
105
+ await settle(largeRegistry);
106
+
107
+ // Ten times the skills, ten times the requests - never ten times the
108
+ // skills in a single request.
109
+ expect(large.embedCalls.length).toBe(10);
110
+ expect(large.embedCalls.length).toBe(small.embedCalls.length * 10);
111
+ });
112
+
113
+ test("only one embedding request is in flight at a time", async () => {
114
+ const indexer = new CountingIndexer();
115
+ const registry = createSkillRegistry({ indexer, batchSize: 4 });
116
+
117
+ let release!: () => void;
118
+ indexer.gate = {
119
+ promise: new Promise<void>((resolve) => {
120
+ release = resolve;
121
+ }),
122
+ release: () => {},
123
+ };
124
+
125
+ for (let i = 0; i < 40; i++) {
126
+ registry.register(makeSkill(`gated-${i}`));
127
+ }
128
+ await new Promise((resolve) => setTimeout(resolve, 50));
129
+
130
+ // The queue waits for the first batch, so no second request started.
131
+ expect(indexer.embedCalls.length).toBe(1);
132
+
133
+ indexer.gate.release = release;
134
+ indexer.gate = null;
135
+ release();
136
+ await settle(registry);
137
+
138
+ expect(indexer.embeddedCount).toBe(40);
139
+ expect(indexer.maxConcurrentEmbeds).toBe(1);
140
+ });
141
+
142
+ test("a skill whose text did not change is not indexed again", async () => {
143
+ const indexer = new CountingIndexer();
144
+ const registry = createSkillRegistry({ indexer });
145
+
146
+ registry.register(makeSkill("stable"));
147
+ await settle(registry);
148
+ expect(indexer.embeddedCount).toBe(1);
149
+
150
+ // The same skill, registered again with the same text.
151
+ registry.register(makeSkill("stable"));
152
+ await settle(registry);
153
+ expect(indexer.embeddedCount).toBe(1);
154
+
155
+ // The same skill with a different body is indexed again.
156
+ registry.register(makeSkill("stable", "a different body"));
157
+ await settle(registry);
158
+ expect(indexer.embeddedCount).toBe(2);
159
+ });
160
+
161
+ test("the points carry the vector of their skill", async () => {
162
+ const indexer = new CountingIndexer();
163
+ const registry = createSkillRegistry({ indexer, batchSize: 2 });
164
+
165
+ registry.register(makeSkill("alpha"));
166
+ registry.register(makeSkill("beta"));
167
+ await settle(registry);
168
+
169
+ const points = indexer.upsertCalls.flat();
170
+ expect(points.length).toBe(2);
171
+ expect(points.map((point) => point.payload.name).sort()).toEqual(["alpha", "beta"]);
172
+ for (const point of points) {
173
+ expect(point.vector.length).toBe(3);
174
+ }
175
+ });
176
+
177
+ test("a registry without an index keeps no vectors", async () => {
178
+ const registry = createSkillRegistry({ indexer: null });
179
+
180
+ registry.register(makeSkill("unindexed"));
181
+ await settle(registry);
182
+
183
+ expect(registry.list().length).toBe(1);
184
+ expect(registry.isQdrantFailed()).toBe(false);
185
+ });
186
+
187
+ test("a failing index does not stop the other skills", async () => {
188
+ let calls = 0;
189
+ const indexer: SkillVectorIndexer = {
190
+ embed: async (texts: string[]) => {
191
+ calls += 1;
192
+ if (calls === 1) throw new Error("the index is unavailable");
193
+ return texts.map(() => [1, 2, 3]);
194
+ },
195
+ upsert: async () => {},
196
+ };
197
+ const registry = createSkillRegistry({ indexer, batchSize: 2 });
198
+
199
+ registry.register(makeSkill("first"));
200
+ registry.register(makeSkill("second"));
201
+ await settle(registry);
202
+
203
+ expect(registry.list().length).toBe(2);
204
+ });
205
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/sprint-planner",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "type": "module",
5
5
  "description": "Sprint planning engine for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/subscription-engine",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/task-compiler",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Task Compiler for pi-harness-runtime — produces deterministic task DAG from CompiledRequirement",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/telegram",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — Telegram inbound/outbound",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/tencentdb-memory",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "TencentDB Agent Memory client - sync skills to centralized knowledge base",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tencentdb-sync",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Sync SKILL.md skills to TencentDB-Agent-Memory server (RFC-0105/0106)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/test-data-generator",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Test data generation for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/test-generator",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Auto-Test Generator — generate unit and E2E tests from source code (RFC-0013)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/todo-bd-sync",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Two-way sync between rpiv-todo overlay and bd issue tracker",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/token-estimation",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — token-estimation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/token-optimizer",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Token Budget Optimizer — maximize prompt value within token limits (RFC-0011)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tooling-preference",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — JS/TS tooling preference (Biome vs ESLint/Prettier) proposal",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/trajectory",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Trajectory capture for the write-review loop — persists cycle records for learning",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tui",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — tui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/two-phase-review",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/types",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "Shared TypeScript types for pi-harness-runtime packages",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/voice-api",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "type": "module",
5
5
  "description": "Voice-to-text API using Whisper for Telegram and LINE integration",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/webmcp-tester",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "WebMCP-powered E2E testing for pi-harness - natural language test scenarios",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/wiki",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "LLM Wiki integration for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/workflow-events",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,5 +1,6 @@
1
+ /// <reference types="node" />
1
2
  import { describe, it, beforeEach, afterEach } from "node:test";
2
- import { equal } from "node:assert";
3
+ import { deepStrictEqual, equal } from "node:assert";
3
4
  import { join } from "node:path";
4
5
  import { mkdirSync, writeFileSync, rmSync } from "node:fs";
5
6
  import { tmpdir } from "node:os";
@@ -23,13 +24,13 @@ describe("WorkspaceScanner", () => {
23
24
  it("skips git when requested", async () => {
24
25
  const scanner = new WorkspaceScanner(testDir);
25
26
  const snap = await scanner.scan({ skipGit: true });
26
- equal(snap.git, null);
27
+ deepStrictEqual(snap.git, null);
27
28
  });
28
29
 
29
30
  it("skips config when requested", async () => {
30
31
  const scanner = new WorkspaceScanner(testDir);
31
32
  const snap = await scanner.scan({ skipConfig: true });
32
- equal(snap.project, {});
33
+ deepStrictEqual(snap.project, {});
33
34
  equal(snap.configFiles.length, 0);
34
35
  });
35
36
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/worktree",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
4
4
  "description": "pi-harness-runtime — worktree",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",