pi-harness-runtime 1.1.412 → 1.1.414

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 (136) hide show
  1. package/harness/skills-startup.ts +27 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  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/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/logging/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/milestone-manager/package.json +1 -1
  55. package/packages/model-registry/package.json +1 -1
  56. package/packages/nextjs-plugin/package.json +1 -1
  57. package/packages/notification/package.json +1 -1
  58. package/packages/observability/package.json +1 -1
  59. package/packages/okf-indexer/package.json +1 -1
  60. package/packages/paperclip-integration/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/permission-mode/package.json +1 -1
  63. package/packages/privilege-broker/package.json +1 -1
  64. package/packages/project-analyzer/package.json +1 -1
  65. package/packages/project-bootstrap/package.json +1 -1
  66. package/packages/projection-engine/package.json +1 -1
  67. package/packages/prompt-compiler/package.json +1 -1
  68. package/packages/prompt-versioning/package.json +1 -1
  69. package/packages/provider-adapter-sdk/package.json +1 -1
  70. package/packages/provider-router/package.json +1 -1
  71. package/packages/provider-selector/package.json +1 -1
  72. package/packages/providers/package.json +1 -1
  73. package/packages/qdrant-skills/dist/client.d.ts +10 -0
  74. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  75. package/packages/qdrant-skills/dist/client.js +20 -1
  76. package/packages/qdrant-skills/dist/client.js.map +1 -1
  77. package/packages/qdrant-skills/dist/index.d.ts +1 -1
  78. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  79. package/packages/qdrant-skills/dist/index.js +1 -1
  80. package/packages/qdrant-skills/dist/index.js.map +1 -1
  81. package/packages/qdrant-skills/package.json +1 -1
  82. package/packages/qdrant-skills/src/client.ts +19 -1
  83. package/packages/qdrant-skills/src/index.ts +1 -0
  84. package/packages/qdrant-skills/test/collection-info.test.ts +37 -1
  85. package/packages/quota-manager/package.json +1 -1
  86. package/packages/rate-limiter/package.json +1 -1
  87. package/packages/react-vite-plugin/package.json +1 -1
  88. package/packages/relay/package.json +1 -1
  89. package/packages/release-manager/package.json +1 -1
  90. package/packages/requirement-compiler/package.json +1 -1
  91. package/packages/runtime/package.json +1 -1
  92. package/packages/scheduler/package.json +1 -1
  93. package/packages/scheduler-adapter/package.json +1 -1
  94. package/packages/session/package.json +1 -1
  95. package/packages/session-api/package.json +1 -1
  96. package/packages/session-export/package.json +1 -1
  97. package/packages/shared-context/package.json +1 -1
  98. package/packages/skill-mcp-client/package.json +1 -1
  99. package/packages/skill-registry/package.json +1 -1
  100. package/packages/skills/dist/loader.d.ts +40 -7
  101. package/packages/skills/dist/loader.d.ts.map +1 -1
  102. package/packages/skills/dist/loader.js +65 -20
  103. package/packages/skills/dist/loader.js.map +1 -1
  104. package/packages/skills/dist/vector-schema.d.ts +89 -4
  105. package/packages/skills/dist/vector-schema.d.ts.map +1 -1
  106. package/packages/skills/dist/vector-schema.js +62 -4
  107. package/packages/skills/dist/vector-schema.js.map +1 -1
  108. package/packages/skills/package.json +1 -1
  109. package/packages/skills/src/loader.ts +102 -25
  110. package/packages/skills/src/vector-schema.ts +117 -4
  111. package/packages/skills/test/remote-skills.test.ts +50 -0
  112. package/packages/skills/test/vector-schema.test.ts +167 -0
  113. package/packages/sprint-planner/package.json +1 -1
  114. package/packages/subscription-engine/package.json +1 -1
  115. package/packages/task-compiler/package.json +1 -1
  116. package/packages/telegram/package.json +1 -1
  117. package/packages/tencentdb-memory/package.json +1 -1
  118. package/packages/tencentdb-sync/package.json +1 -1
  119. package/packages/test-data-generator/package.json +1 -1
  120. package/packages/test-generator/package.json +1 -1
  121. package/packages/todo-bd-sync/package.json +1 -1
  122. package/packages/token-estimation/package.json +1 -1
  123. package/packages/token-optimizer/package.json +1 -1
  124. package/packages/tooling-preference/package.json +1 -1
  125. package/packages/trajectory/package.json +1 -1
  126. package/packages/tui/package.json +1 -1
  127. package/packages/two-phase-review/package.json +1 -1
  128. package/packages/types/package.json +1 -1
  129. package/packages/voice-api/package.json +1 -1
  130. package/packages/webmcp-tester/package.json +1 -1
  131. package/packages/wiki/package.json +1 -1
  132. package/packages/workflow-events/package.json +1 -1
  133. package/packages/workspace-scanner/package.json +1 -1
  134. package/packages/worktree/package.json +1 -1
  135. package/packages/write-review/package.json +1 -1
  136. package/scripts/sync-to-qdrant.ts +19 -13
@@ -22,6 +22,10 @@ import {
22
22
  LEGACY_SKILL_COLLECTION_MODEL,
23
23
  SKILL_COLLECTION,
24
24
  describeSkillSchemaMismatch,
25
+ skillPointPayload,
26
+ skillQdrantPoint,
27
+ stableHash,
28
+ type SkillPointPayload,
25
29
  type SkillVectorSchema,
26
30
  } from "./vector-schema.js";
27
31
  import {
@@ -39,11 +43,15 @@ import {
39
43
  // At runtime, packages/ is resolved from the monorepo root or node_modules.
40
44
 
41
45
  export interface QdrantSkillPoint {
46
+ /**
47
+ * The id of the point.
48
+ *
49
+ * It is the hash of the name in the payload, so the point id and the
50
+ * payload name always belong together.
51
+ */
42
52
  id: number;
43
- name: string;
44
- description: string;
45
- body: string;
46
- text: string;
53
+ /** The payload of the point. */
54
+ payload: SkillPointPayload;
47
55
  }
48
56
 
49
57
  export interface QdrantClientLike {
@@ -98,6 +106,29 @@ export interface QdrantSkillStats {
98
106
  * nothing does not have to guess why.
99
107
  */
100
108
  schemaMismatch?: string;
109
+ /**
110
+ * The collection of an earlier installation that still holds the skills.
111
+ *
112
+ * The runtime collection is empty (or does not exist yet) while the
113
+ * legacy collection holds the skills of the previous version. They are
114
+ * not lost - they live in a collection with another name, and one with
115
+ * another vector size.
116
+ */
117
+ legacy?: LegacyCollectionInfo;
118
+ }
119
+
120
+ /**
121
+ * The skills of the legacy collection.
122
+ */
123
+ export interface LegacyCollectionInfo {
124
+ /** The name of the collection. */
125
+ collection: string;
126
+ /** The number of skill vectors it holds. */
127
+ count: number;
128
+ /** The vector size it was created with. */
129
+ dimensions?: number;
130
+ /** The embedding model it was created with. */
131
+ model?: string;
101
132
  }
102
133
 
103
134
  /** The information the Qdrant package can report about a collection. */
@@ -126,7 +157,7 @@ const RUNTIME_VECTOR_DISTANCE = "Cosine";
126
157
  export interface SkillVectorPoint {
127
158
  id: number;
128
159
  vector: number[];
129
- payload: QdrantSkillPoint;
160
+ payload: SkillPointPayload;
130
161
  }
131
162
 
132
163
  /**
@@ -433,6 +464,43 @@ export class SkillRegistry {
433
464
  }
434
465
  }
435
466
 
467
+ /**
468
+ * The skills of the legacy collection.
469
+ *
470
+ * The legacy collection holds 384-dimensional vectors, so it cannot be
471
+ * searched with the vectors of the runtime. Its skills are not lost -
472
+ * they live in a collection with another name.
473
+ */
474
+ private async _readLegacyCollection(
475
+ client: QdrantClientLike,
476
+ mods: QdrantModules,
477
+ ): Promise<LegacyCollectionInfo | undefined> {
478
+ // The legacy collection IS the runtime collection when the user
479
+ // configured it explicitly - then there is no second collection.
480
+ if (this._options.collection === LEGACY_SKILL_COLLECTION) return undefined;
481
+ if (!mods.getCollectionInfo) return undefined;
482
+
483
+ try {
484
+ const info = await mods.getCollectionInfo(client, LEGACY_SKILL_COLLECTION);
485
+ if (!info?.exists) return undefined;
486
+ const count = info.pointsCount ?? 0;
487
+ if (count === 0) return undefined;
488
+ return {
489
+ collection: LEGACY_SKILL_COLLECTION,
490
+ count,
491
+ dimensions: info.vectorsSize,
492
+ model:
493
+ info.vectorsSize === LEGACY_SKILL_COLLECTION_DIMENSIONS
494
+ ? LEGACY_SKILL_COLLECTION_MODEL
495
+ : undefined,
496
+ };
497
+ } catch {
498
+ // The legacy collection is a bonus - a failure to read it must not
499
+ // hide the state of the runtime collection.
500
+ return undefined;
501
+ }
502
+ }
503
+
436
504
  /**
437
505
  * The schema the runtime expects of the skill collection.
438
506
  *
@@ -500,7 +568,15 @@ export class SkillRegistry {
500
568
  };
501
569
  }
502
570
  if (!info.exists) {
503
- return { state: "missing", count: 0, collection: config.collection };
571
+ // The runtime collection does not exist (yet). An earlier
572
+ // installation may have left the skills in the legacy collection -
573
+ // naming them is more useful than an empty count.
574
+ return {
575
+ state: "missing",
576
+ count: 0,
577
+ collection: config.collection,
578
+ legacy: await this._readLegacyCollection(client, mods),
579
+ };
504
580
  }
505
581
 
506
582
  const count = info.pointsCount ?? 0;
@@ -512,6 +588,12 @@ export class SkillRegistry {
512
588
  distance: info.vectorsDistance,
513
589
  };
514
590
 
591
+ // An empty runtime collection can still have its skills in the
592
+ // legacy one - the same information, one collection further down.
593
+ if (count === 0) {
594
+ stats.legacy = await this._readLegacyCollection(client, mods);
595
+ }
596
+
515
597
  const mismatch = describeSkillSchemaMismatch(
516
598
  { dimensions: info.vectorsSize, distance: info.vectorsDistance },
517
599
  this._expectedVectorSchema(mods),
@@ -539,13 +621,17 @@ export class SkillRegistry {
539
621
 
540
622
  /** The point of a skill in the vector index. */
541
623
  private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
542
- return {
543
- id: this._hashId(skill.id),
624
+ // The payload and the point id come from the shared schema, so the
625
+ // runtime and `scripts/sync-to-qdrant.ts` store the same point for the
626
+ // same skill. A writer that used its own id would leave two points
627
+ // behind - or overwrite an unrelated one.
628
+ const payload = skillPointPayload({
544
629
  name: skill.id,
630
+ title: skill.frontmatter.name,
545
631
  description: skill.frontmatter.description,
546
- body: skill.body.slice(0, 2000),
547
- text: [skill.id, skill.frontmatter.name, skill.frontmatter.description, skill.body.slice(0, 1500)].join(" "),
548
- };
632
+ body: skill.body,
633
+ });
634
+ return skillQdrantPoint(payload);
549
635
  }
550
636
 
551
637
  /**
@@ -603,7 +689,7 @@ export class SkillRegistry {
603
689
  */
604
690
  private _scheduleIndex(skill: Skill): void {
605
691
  const point = this._toQdrantPoint(skill);
606
- const hash = this._hashId(point.text);
692
+ const hash = stableHash(point.payload.text);
607
693
  if (this._indexedHashes.get(skill.id) === hash) return;
608
694
 
609
695
  this._indexedHashes.set(skill.id, hash);
@@ -655,12 +741,12 @@ export class SkillRegistry {
655
741
  while (this._indexQueue.length > 0) {
656
742
  const batch = this._indexQueue.splice(0, this._batchSize);
657
743
  try {
658
- const vectors = await indexer.embed(batch.map((item) => item.point.text));
744
+ const vectors = await indexer.embed(batch.map((item) => item.point.payload.text));
659
745
  const points: SkillVectorPoint[] = [];
660
746
  batch.forEach((item, position) => {
661
747
  const vector = vectors[position];
662
748
  if (!vector || vector.length === 0) return;
663
- points.push({ id: item.point.id, vector, payload: item.point });
749
+ points.push({ id: item.point.id, vector, payload: item.point.payload });
664
750
  });
665
751
  if (points.length > 0) await indexer.upsert(points);
666
752
  } catch {
@@ -675,15 +761,6 @@ export class SkillRegistry {
675
761
  }
676
762
  }
677
763
 
678
- /** Simple deterministic ID for Qdrant (no bigints needed). */
679
- private _hashId(id: string): number {
680
- let h = 0;
681
- for (let i = 0; i < id.length; i++) {
682
- h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
683
- }
684
- return Math.abs(h);
685
- }
686
-
687
764
  /**
688
765
  * Register a skill (auto-indexes to Qdrant if configured).
689
766
  */
@@ -1010,12 +1087,12 @@ export class SkillRegistry {
1010
1087
  const batch = skills.slice(i, i + this._batchSize);
1011
1088
  const points = batch.map((skill) => this._toQdrantPoint(skill));
1012
1089
  try {
1013
- const vectors = await indexer.embed(points.map((point) => point.text));
1090
+ const vectors = await indexer.embed(points.map((point) => point.payload.text));
1014
1091
  const payload: SkillVectorPoint[] = [];
1015
1092
  points.forEach((point, position) => {
1016
1093
  const vector = vectors[position];
1017
1094
  if (!vector || vector.length === 0) return;
1018
- payload.push({ id: point.id, vector, payload: point });
1095
+ payload.push({ id: point.id, vector, payload: point.payload });
1019
1096
  });
1020
1097
  if (payload.length > 0) await indexer.upsert(payload);
1021
1098
  } catch {
@@ -51,12 +51,125 @@ export interface SkillVectorSchema {
51
51
  }
52
52
 
53
53
  /**
54
- * Why the collection does not match the expected schema - or null when it
55
- * does.
54
+ * The payload of a skill point.
56
55
  *
57
- * The message names the numbers, so the reader can tell WHICH schema the
58
- * collection was built with.
56
+ * Both writers of the collection - the runtime registry and
57
+ * `scripts/sync-to-qdrant.ts` - store the SAME payload under the SAME
58
+ * point id. A point is addressed by its id, so a writer that used a
59
+ * different id for the same skill would leave two points behind, and a
60
+ * writer whose id happened to collide with a foreign one would overwrite
61
+ * an unrelated skill.
59
62
  */
63
+ export type SkillPointPayload = {
64
+ /** The name of the skill. The point id is derived from it. */
65
+ name: string;
66
+ /** What the skill does. */
67
+ description: string;
68
+ /** The body of the skill. */
69
+ body: string;
70
+ /** The text the vector was built from. */
71
+ text: string;
72
+ };
73
+
74
+ /**
75
+ * The body of a payload is truncated.
76
+ *
77
+ * A payload is an index, not a file store - the complete skill comes from
78
+ * the skill service.
79
+ */
80
+ export const SKILL_PAYLOAD_BODY_LIMIT = 2000;
81
+
82
+ /** The text of a payload is truncated as well. */
83
+ export const SKILL_PAYLOAD_TEXT_LIMIT = 1500;
84
+
85
+ /**
86
+ * A stable hash of a string.
87
+ *
88
+ * The same string always produces the same number, in every process and
89
+ * on every machine - a point id has to survive a restart.
90
+ *
91
+ * Two uses:
92
+ * - the id of a point is the hash of the name its payload carries, so the
93
+ * point id and the payload name always belong together. Two writers
94
+ * that store the same name address the same point.
95
+ * - the hash of the text of a skill says whether its content changed.
96
+ */
97
+ export function stableHash(value: string): number {
98
+ let hash = 0;
99
+ for (let i = 0; i < value.length; i++) {
100
+ hash = (Math.imul(31, hash) + value.charCodeAt(i)) | 0;
101
+ }
102
+ return Math.abs(hash);
103
+ }
104
+
105
+ /**
106
+ * The text a skill vector is built from.
107
+ *
108
+ * The title is added when it differs from the name: a skill whose
109
+ * directory is named differently from its title carries both.
110
+ */
111
+ export function skillPointText(input: {
112
+ /** The name the payload carries - the identity of the skill. */
113
+ name: string;
114
+ /** The title of the skill, when it differs from the name. */
115
+ title?: string;
116
+ /** What the skill does. */
117
+ description: string;
118
+ /** The body of the skill. */
119
+ body: string;
120
+ }): string {
121
+ const parts = [input.name];
122
+ if (input.title && input.title !== input.name) parts.push(input.title);
123
+ parts.push(input.description);
124
+ parts.push(input.body.slice(0, SKILL_PAYLOAD_TEXT_LIMIT));
125
+ return parts.join(" ");
126
+ }
127
+
128
+ /**
129
+ * A point of the skill collection.
130
+ *
131
+ * The id is the hash of the name the payload carries, so the point id and
132
+ * the payload name always belong together: two writers that store the
133
+ * same name address the same point.
134
+ */
135
+ export interface SkillQdrantPoint {
136
+ id: number;
137
+ payload: SkillPointPayload;
138
+ }
139
+
140
+ /**
141
+ * The point of a skill.
142
+ *
143
+ * The runtime registry and `scripts/sync-to-qdrant.ts` both build their
144
+ * points here, so they cannot drift apart. A writer that used its own id
145
+ * would leave a second point for every skill the other one indexes, and
146
+ * an id that happens to collide with a foreign one would overwrite an
147
+ * unrelated skill.
148
+ */
149
+ export function skillQdrantPoint(payload: SkillPointPayload): SkillQdrantPoint {
150
+ return { id: stableHash(payload.name), payload };
151
+ }
152
+
153
+ /**
154
+ * The payload of a skill point.
155
+ */
156
+ export function skillPointPayload(input: {
157
+ /** The name the payload carries - the identity of the skill. */
158
+ name: string;
159
+ /** The title of the skill, when it differs from the name. */
160
+ title?: string;
161
+ /** What the skill does. */
162
+ description: string;
163
+ /** The body of the skill. */
164
+ body: string;
165
+ }): SkillPointPayload {
166
+ return {
167
+ name: input.name,
168
+ description: input.description,
169
+ body: input.body.slice(0, SKILL_PAYLOAD_BODY_LIMIT),
170
+ text: skillPointText(input),
171
+ };
172
+ }
60
173
  export function describeSkillSchemaMismatch(
61
174
  actual: { dimensions?: number; distance?: string },
62
175
  expected: SkillVectorSchema,
@@ -239,4 +239,54 @@ describe("the state of the vector store", () => {
239
239
  expect(stats.state).toBe("missing");
240
240
  expect(stats.count).toBe(0);
241
241
  });
242
+
243
+ it("names the skills of the legacy collection", async () => {
244
+ // The runtime collection does not exist yet, but the collection of
245
+ // the previous version still holds the skills. They are not lost -
246
+ // the reader has to know where they are.
247
+ const client = {
248
+ async upsert(): Promise<void> {},
249
+ async query(): Promise<unknown> {
250
+ return { points: [] };
251
+ },
252
+ async getCollection(name: string): Promise<unknown> {
253
+ if (name === "pi-harness-skills") {
254
+ return {
255
+ points_count: 79,
256
+ config: { params: { vectors: { size: 384, distance: "Cosine" } } },
257
+ };
258
+ }
259
+ throw new Error("Not Found");
260
+ },
261
+ };
262
+ const registry = createSkillRegistry({
263
+ qdrantClient: client as never,
264
+ embedder: fakeEmbedder,
265
+ collection: "pi-harness-skills-openai-v1",
266
+ });
267
+
268
+ const stats = await registry.getQdrantStats();
269
+
270
+ expect(stats.state).toBe("missing");
271
+ expect(stats.legacy?.collection).toBe("pi-harness-skills");
272
+ expect(stats.legacy?.count).toBe(79);
273
+ expect(stats.legacy?.dimensions).toBe(384);
274
+ expect(stats.legacy?.model).toBe("sentence-transformers/all-MiniLM-L6-v2");
275
+ });
276
+
277
+ it("does not look for a second collection when the legacy one is the runtime one", async () => {
278
+ const client = fakeClient([], {
279
+ points_count: 0,
280
+ config: { params: { vectors: { size: 1536, distance: "Cosine" } } },
281
+ });
282
+ const registry = createSkillRegistry({
283
+ qdrantClient: client,
284
+ embedder: fakeEmbedder,
285
+ collection: "pi-harness-skills",
286
+ });
287
+
288
+ const stats = await registry.getQdrantStats();
289
+
290
+ expect(stats.legacy).toBeUndefined();
291
+ });
242
292
  });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Regression tests for the point identity of the skill collection.
3
+ *
4
+ * Two writers fill the same collection: the runtime registry and
5
+ * `scripts/sync-to-qdrant.ts`. They used to disagree about the identity of
6
+ * a point - the runtime hashed the name of the skill, the script used a
7
+ * running number. The same skill then existed twice, and a running number
8
+ * could overwrite a point of an unrelated skill.
9
+ *
10
+ * The identity now comes from one shared place, and these tests hold it
11
+ * there.
12
+ */
13
+
14
+ import { describe, expect, test } from "bun:test";
15
+
16
+ import {
17
+ createSkillRegistry,
18
+ type SkillVectorIndexer,
19
+ type SkillVectorPoint,
20
+ } from "../src/loader.js";
21
+ import {
22
+ SKILL_PAYLOAD_BODY_LIMIT,
23
+ SKILL_PAYLOAD_TEXT_LIMIT,
24
+ skillPointPayload,
25
+ skillQdrantPoint,
26
+ stableHash,
27
+ } from "../src/vector-schema.js";
28
+ import type { Skill } from "../src/types.js";
29
+
30
+ /** Wait until the registry has no index work left. */
31
+ async function settle(registry: { isIndexing(): boolean }): Promise<void> {
32
+ for (let attempt = 0; attempt < 200 && registry.isIndexing(); attempt++) {
33
+ await new Promise((resolve) => setTimeout(resolve, 5));
34
+ }
35
+ }
36
+
37
+ /** A skill with the given id and body. */
38
+ function makeSkill(id: string, body = `body of ${id}`): Skill {
39
+ return {
40
+ id,
41
+ path: `/tmp/skills/${id}`,
42
+ frontmatter: {
43
+ name: id,
44
+ description: `the ${id} skill`,
45
+ },
46
+ body,
47
+ metadata: {} as Skill["metadata"],
48
+ } as Skill;
49
+ }
50
+
51
+ /** An indexer that keeps the points it receives. */
52
+ class RecordingIndexer implements SkillVectorIndexer {
53
+ readonly upsertCalls: SkillVectorPoint[][] = [];
54
+
55
+ async embed(texts: string[]): Promise<number[][]> {
56
+ return texts.map((text) => [text.length, 1, 2]);
57
+ }
58
+
59
+ async upsert(points: SkillVectorPoint[]): Promise<void> {
60
+ this.upsertCalls.push(points);
61
+ }
62
+ }
63
+
64
+ describe("the identity of a skill point", () => {
65
+ test("the id of the point is the hash of the name in the payload", () => {
66
+ const payload = skillPointPayload({
67
+ name: "a-skill",
68
+ description: "does something",
69
+ body: "the body",
70
+ });
71
+ const point = skillQdrantPoint(payload);
72
+
73
+ expect(point.id).toBe(stableHash(payload.name));
74
+ });
75
+
76
+ test("the same name always addresses the same point", () => {
77
+ const first = skillQdrantPoint(
78
+ skillPointPayload({ name: "a-skill", description: "one", body: "one" }),
79
+ );
80
+ const second = skillQdrantPoint(
81
+ skillPointPayload({ name: "a-skill", description: "two", body: "two" }),
82
+ );
83
+
84
+ // The content may change, the identity of the point may not.
85
+ expect(second.id).toBe(first.id);
86
+ });
87
+
88
+ test("different names address different points", () => {
89
+ const first = skillQdrantPoint(
90
+ skillPointPayload({ name: "a-skill", description: "d", body: "b" }),
91
+ );
92
+ const second = skillQdrantPoint(
93
+ skillPointPayload({ name: "b-skill", description: "d", body: "b" }),
94
+ );
95
+
96
+ expect(second.id).not.toBe(first.id);
97
+ });
98
+
99
+ test("the payload carries the shared fields and nothing else", () => {
100
+ const payload = skillPointPayload({
101
+ name: "a-skill",
102
+ title: "A Skill",
103
+ description: "does something",
104
+ body: "the body",
105
+ });
106
+
107
+ expect(Object.keys(payload).sort()).toEqual([
108
+ "body",
109
+ "description",
110
+ "name",
111
+ "text",
112
+ ]);
113
+ });
114
+
115
+ test("the payload and the text are truncated", () => {
116
+ const payload = skillPointPayload({
117
+ name: "a-skill",
118
+ description: "d",
119
+ body: "x".repeat(SKILL_PAYLOAD_BODY_LIMIT + 500),
120
+ });
121
+
122
+ expect(payload.body.length).toBe(SKILL_PAYLOAD_BODY_LIMIT);
123
+ expect(payload.text.length).toBeLessThanOrEqual(
124
+ SKILL_PAYLOAD_TEXT_LIMIT + payload.name.length + payload.description.length + 10,
125
+ );
126
+ });
127
+
128
+ test("the title is part of the text when it differs from the name", () => {
129
+ const withTitle = skillPointPayload({
130
+ name: "a-skill",
131
+ title: "A Human Title",
132
+ description: "d",
133
+ body: "b",
134
+ });
135
+ const withoutTitle = skillPointPayload({
136
+ name: "a-skill",
137
+ description: "d",
138
+ body: "b",
139
+ });
140
+
141
+ expect(withTitle.text).toContain("A Human Title");
142
+ expect(withoutTitle.text).not.toContain("A Human Title");
143
+ });
144
+ });
145
+
146
+ describe("the runtime writes the points of the shared schema", () => {
147
+ test("the indexed point carries the shared payload and id", async () => {
148
+ const indexer = new RecordingIndexer();
149
+ const registry = createSkillRegistry({ indexer });
150
+
151
+ registry.register(makeSkill("alpha"));
152
+ await settle(registry);
153
+
154
+ const points = indexer.upsertCalls.flat();
155
+ expect(points.length).toBe(1);
156
+
157
+ const point = points[0];
158
+ expect(point.id).toBe(stableHash(point.payload.name));
159
+ expect(point.payload.name).toBe("alpha");
160
+ expect(Object.keys(point.payload).sort()).toEqual([
161
+ "body",
162
+ "description",
163
+ "name",
164
+ "text",
165
+ ]);
166
+ });
167
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/sprint-planner",
3
- "version": "1.1.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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.412",
3
+ "version": "1.1.414",
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",