pi-harness-runtime 1.1.411 → 1.1.413

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 (157) 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 +229 -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/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/connection-runtime/package.json +1 -1
  27. package/packages/context-compiler/package.json +1 -1
  28. package/packages/context-discovery/package.json +1 -1
  29. package/packages/context-manager/package.json +1 -1
  30. package/packages/contracts/package.json +1 -1
  31. package/packages/cookie-sanitizer/package.json +1 -1
  32. package/packages/cost-optimizer/package.json +1 -1
  33. package/packages/dependency-analyzer/package.json +1 -1
  34. package/packages/django-plugin/package.json +1 -1
  35. package/packages/doc-generator/package.json +1 -1
  36. package/packages/durable-review/package.json +1 -1
  37. package/packages/e2e-tester/package.json +1 -1
  38. package/packages/evaluation-engine/package.json +1 -1
  39. package/packages/evaluation-runner/package.json +1 -1
  40. package/packages/event-bus/package.json +1 -1
  41. package/packages/event-sink/package.json +1 -1
  42. package/packages/event-store/package.json +1 -1
  43. package/packages/experience-replay/package.json +1 -1
  44. package/packages/feedback-collector/package.json +1 -1
  45. package/packages/file-copy-helper/package.json +1 -1
  46. package/packages/framework-detector/package.json +1 -1
  47. package/packages/framework-plugin-sdk/package.json +1 -1
  48. package/packages/frappe-plugin/package.json +1 -1
  49. package/packages/generic-web-plugin/package.json +1 -1
  50. package/packages/health-monitor/package.json +1 -1
  51. package/packages/intent-analyzer/package.json +1 -1
  52. package/packages/jev-judge/package.json +1 -1
  53. package/packages/knowledge-graph/package.json +1 -1
  54. package/packages/knowledge-retrieval/package.json +1 -1
  55. package/packages/laravel-plugin/package.json +1 -1
  56. package/packages/learning-engine/package.json +1 -1
  57. package/packages/logging/package.json +1 -1
  58. package/packages/mcp-adapter/package.json +1 -1
  59. package/packages/memory-engine/package.json +1 -1
  60. package/packages/milestone-manager/package.json +1 -1
  61. package/packages/model-registry/package.json +1 -1
  62. package/packages/nextjs-plugin/package.json +1 -1
  63. package/packages/notification/package.json +1 -1
  64. package/packages/observability/package.json +1 -1
  65. package/packages/okf-indexer/package.json +1 -1
  66. package/packages/paperclip-integration/package.json +1 -1
  67. package/packages/performance-optimizer/package.json +1 -1
  68. package/packages/permission-mode/package.json +1 -1
  69. package/packages/privilege-broker/package.json +1 -1
  70. package/packages/project-analyzer/package.json +1 -1
  71. package/packages/project-bootstrap/package.json +1 -1
  72. package/packages/projection-engine/package.json +1 -1
  73. package/packages/prompt-compiler/package.json +1 -1
  74. package/packages/prompt-versioning/package.json +1 -1
  75. package/packages/provider-adapter-sdk/package.json +1 -1
  76. package/packages/provider-router/package.json +1 -1
  77. package/packages/provider-selector/package.json +1 -1
  78. package/packages/providers/package.json +1 -1
  79. package/packages/qdrant-skills/dist/client.d.ts +10 -0
  80. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  81. package/packages/qdrant-skills/dist/client.js +41 -9
  82. package/packages/qdrant-skills/dist/client.js.map +1 -1
  83. package/packages/qdrant-skills/dist/embedder.d.ts +10 -0
  84. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  85. package/packages/qdrant-skills/dist/embedder.js +12 -2
  86. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  87. package/packages/qdrant-skills/dist/index.d.ts +2 -2
  88. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  89. package/packages/qdrant-skills/dist/index.js +2 -2
  90. package/packages/qdrant-skills/dist/index.js.map +1 -1
  91. package/packages/qdrant-skills/package.json +1 -1
  92. package/packages/qdrant-skills/src/client.ts +38 -9
  93. package/packages/qdrant-skills/src/embedder.ts +14 -2
  94. package/packages/qdrant-skills/src/index.ts +3 -0
  95. package/packages/qdrant-skills/test/collection-info.test.ts +165 -0
  96. package/packages/quota-manager/package.json +1 -1
  97. package/packages/rate-limiter/package.json +1 -1
  98. package/packages/react-vite-plugin/package.json +1 -1
  99. package/packages/relay/package.json +1 -1
  100. package/packages/release-manager/package.json +1 -1
  101. package/packages/requirement-compiler/package.json +1 -1
  102. package/packages/runtime/package.json +1 -1
  103. package/packages/scheduler/package.json +1 -1
  104. package/packages/scheduler-adapter/package.json +1 -1
  105. package/packages/session/package.json +1 -1
  106. package/packages/session-api/package.json +1 -1
  107. package/packages/session-export/package.json +1 -1
  108. package/packages/shared-context/package.json +1 -1
  109. package/packages/skill-mcp-client/package.json +1 -1
  110. package/packages/skill-registry/package.json +1 -1
  111. package/packages/skills/dist/index.d.ts +14 -1
  112. package/packages/skills/dist/index.d.ts.map +1 -1
  113. package/packages/skills/dist/index.js +3 -1
  114. package/packages/skills/dist/index.js.map +1 -1
  115. package/packages/skills/dist/loader.d.ts +209 -0
  116. package/packages/skills/dist/loader.d.ts.map +1 -1
  117. package/packages/skills/dist/loader.js +386 -15
  118. package/packages/skills/dist/loader.js.map +1 -1
  119. package/packages/skills/dist/remote-source.d.ts +107 -0
  120. package/packages/skills/dist/remote-source.d.ts.map +1 -0
  121. package/packages/skills/dist/remote-source.js +79 -0
  122. package/packages/skills/dist/remote-source.js.map +1 -0
  123. package/packages/skills/dist/vector-schema.d.ts +58 -0
  124. package/packages/skills/dist/vector-schema.d.ts.map +1 -0
  125. package/packages/skills/dist/vector-schema.js +55 -0
  126. package/packages/skills/dist/vector-schema.js.map +1 -0
  127. package/packages/skills/package.json +1 -1
  128. package/packages/skills/src/index.ts +16 -2
  129. package/packages/skills/src/loader.ts +532 -39
  130. package/packages/skills/src/remote-source.ts +148 -0
  131. package/packages/skills/src/vector-schema.ts +72 -0
  132. package/packages/skills/test/remote-skills.test.ts +292 -0
  133. package/packages/skills/test/remote-source.test.ts +248 -0
  134. package/packages/sprint-planner/package.json +1 -1
  135. package/packages/subscription-engine/package.json +1 -1
  136. package/packages/task-compiler/package.json +1 -1
  137. package/packages/telegram/package.json +1 -1
  138. package/packages/tencentdb-memory/package.json +1 -1
  139. package/packages/tencentdb-sync/package.json +1 -1
  140. package/packages/test-data-generator/package.json +1 -1
  141. package/packages/test-generator/package.json +1 -1
  142. package/packages/todo-bd-sync/package.json +1 -1
  143. package/packages/token-estimation/package.json +1 -1
  144. package/packages/token-optimizer/package.json +1 -1
  145. package/packages/tooling-preference/package.json +1 -1
  146. package/packages/trajectory/package.json +1 -1
  147. package/packages/tui/package.json +1 -1
  148. package/packages/two-phase-review/package.json +1 -1
  149. package/packages/types/package.json +1 -1
  150. package/packages/voice-api/package.json +1 -1
  151. package/packages/webmcp-tester/package.json +1 -1
  152. package/packages/wiki/package.json +1 -1
  153. package/packages/workflow-events/package.json +1 -1
  154. package/packages/workspace-scanner/package.json +1 -1
  155. package/packages/worktree/package.json +1 -1
  156. package/packages/write-review/package.json +1 -1
  157. package/scripts/sync-to-qdrant.ts +79 -14
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/sprint-planner",
3
- "version": "1.1.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
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.411",
3
+ "version": "1.1.413",
4
4
  "description": "Two-agent write-review loop with blackboard coordination",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,6 +14,21 @@
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, basename } from "node:path";
16
16
  import { QdrantClient } from "@qdrant/js-client-rest";
17
+ // The runtime and this script must write into the SAME collection with the
18
+ // SAME vectors: a collection holds vectors of exactly one size, and a vector
19
+ // of a different size cannot be searched with the vectors of the other one.
20
+ // The embedder and the collection name are therefore imported from the
21
+ // packages that own them - they are not repeated here.
22
+ import {
23
+ EMBEDDING_DIMENSIONS,
24
+ EMBEDDING_MODEL,
25
+ createEmbedding,
26
+ createEmbeddingsBatch,
27
+ } from "../packages/qdrant-skills/dist/index.js";
28
+ import {
29
+ LEGACY_SKILL_COLLECTION,
30
+ SKILL_COLLECTION,
31
+ } from "../packages/skills/dist/vector-schema.js";
17
32
 
18
33
  // =============================================================================
19
34
  // SANITIZATION: Replace client-specific names with placeholders
@@ -81,17 +96,17 @@ interface QdrantConfig {
81
96
  function getQdrantConfig(): QdrantConfig | null {
82
97
  let apiKey = process.env.QDRANT_API_KEY;
83
98
  const url = process.env.QDRANT_CLUSTER_ENDPOINT || "https://api.qdrant.tech";
84
- const collection = process.env.QDRANT_COLLECTION || "pi-harness-skills";
85
-
99
+ const collection = process.env.QDRANT_COLLECTION || SKILL_COLLECTION;
100
+
86
101
  if (!apiKey) {
87
102
  const keyPath = `${process.env.HOME}/.pi-harness-runtime/keys/qdrant-api-key.txt`;
88
103
  if (existsSync(keyPath)) {
89
104
  apiKey = readFileSync(keyPath, "utf-8").trim();
90
105
  }
91
106
  }
92
-
107
+
93
108
  if (!apiKey) return null;
94
-
109
+
95
110
  return { url, apiKey, collection };
96
111
  }
97
112
 
@@ -180,15 +195,58 @@ function findSkillDirs(dir: string): string[] {
180
195
  // =============================================================================
181
196
 
182
197
  async function ensureCollection(client: QdrantClient, name: string): Promise<void> {
198
+ const existing = await collectionSchema(client, name);
199
+ if (existing?.dimensions !== undefined && existing.dimensions !== EMBEDDING_DIMENSIONS) {
200
+ // The collection holds vectors of another size. Writing into it would
201
+ // fail, and recreating it would destroy the skills of the other model.
202
+ // The name says so the user can act.
203
+ console.error(
204
+ `[Sync] The collection ${name} holds ${existing.dimensions}-dimensional vectors,` +
205
+ ` but the runtime embeds with ${EMBEDDING_DIMENSIONS} (${EMBEDDING_MODEL}).`,
206
+ );
207
+ if (name === LEGACY_SKILL_COLLECTION) {
208
+ console.error(
209
+ `[Sync] ${LEGACY_SKILL_COLLECTION} is the legacy collection of the MiniLM model.`,
210
+ );
211
+ }
212
+ console.error(
213
+ `[Sync] Use a different collection (QDRANT_COLLECTION) or re-create this one.`,
214
+ );
215
+ process.exit(1);
216
+ }
183
217
  try {
184
218
  await client.createCollection(name, {
185
- vectors: { size: 384, distance: "Cosine" }
219
+ vectors: { size: EMBEDDING_DIMENSIONS, distance: "Cosine" }
186
220
  });
187
221
  } catch {
188
222
  // Already exists - OK
189
223
  }
190
224
  }
191
225
 
226
+ /**
227
+ * The vector size and the distance of a collection.
228
+ *
229
+ * `null` means the collection does not exist (or cannot be read).
230
+ */
231
+ async function collectionSchema(
232
+ client: QdrantClient,
233
+ name: string,
234
+ ): Promise<{ dimensions?: number; distance?: string } | null> {
235
+ try {
236
+ // SAFETY: the Qdrant client answers with the collection description;
237
+ // the shape below is the one field this function reads. The cast is
238
+ // needed because the generated client types the response as `unknown`.
239
+ const info = (await client.getCollection(name)) as unknown as {
240
+ config?: { params?: { vectors?: { size?: number; distance?: string } } };
241
+ };
242
+ const vectors = info?.config?.params?.vectors;
243
+ if (!vectors) return { };
244
+ return { dimensions: vectors.size, distance: vectors.distance };
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
192
250
  async function clearCollection(client: QdrantClient, name: string): Promise<void> {
193
251
  try {
194
252
  await client.deleteCollection(name);
@@ -203,6 +261,17 @@ async function upsertSkills(
203
261
  collectionName: string,
204
262
  skills: SkillDoc[]
205
263
  ): Promise<void> {
264
+ // The vectors are produced by the same embedder the runtime uses, so the
265
+ // two can search each other's points. The embedding happens in batches:
266
+ // one request per 64 skills instead of one per skill.
267
+ const { embeddings } = await createEmbeddingsBatch(skills.map((skill) => skill.text));
268
+ if (embeddings.length !== skills.length) {
269
+ console.error(
270
+ `[Sync] The embedder returned ${embeddings.length} vectors for ${skills.length} skills.`,
271
+ );
272
+ process.exit(1);
273
+ }
274
+
206
275
  const points = skills.map((skill, idx) => ({
207
276
  id: idx + 1,
208
277
  payload: {
@@ -210,12 +279,9 @@ async function upsertSkills(
210
279
  description: skill.description,
211
280
  body: skill.body,
212
281
  },
213
- vector: {
214
- text: skill.text,
215
- model: "sentence-transformers/all-MiniLM-L6-v2"
216
- }
282
+ vector: embeddings[idx],
217
283
  }));
218
-
284
+
219
285
  await client.upsert(collectionName, { wait: true, points });
220
286
  }
221
287
 
@@ -224,11 +290,10 @@ async function searchSkills(
224
290
  collectionName: string,
225
291
  query: string
226
292
  ): Promise<void> {
293
+ // The query is embedded with the same model as the stored vectors.
294
+ const { embedding } = await createEmbedding(query);
227
295
  const results = await client.query(collectionName, {
228
- query: {
229
- text: query,
230
- model: "sentence-transformers/all-MiniLM-L6-v2"
231
- },
296
+ query: embedding,
232
297
  limit: 10,
233
298
  with_payload: true,
234
299
  } as Parameters<typeof client.query>[1]);