pi-harness-runtime 1.1.413 → 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 (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/logging/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/package.json +1 -1
  60. package/packages/performance-optimizer/package.json +1 -1
  61. package/packages/permission-mode/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/relay/package.json +1 -1
  77. package/packages/release-manager/package.json +1 -1
  78. package/packages/requirement-compiler/package.json +1 -1
  79. package/packages/runtime/package.json +1 -1
  80. package/packages/scheduler/package.json +1 -1
  81. package/packages/scheduler-adapter/package.json +1 -1
  82. package/packages/session/package.json +1 -1
  83. package/packages/session-api/package.json +1 -1
  84. package/packages/session-export/package.json +1 -1
  85. package/packages/shared-context/package.json +1 -1
  86. package/packages/skill-mcp-client/package.json +1 -1
  87. package/packages/skill-registry/package.json +1 -1
  88. package/packages/skills/dist/loader.d.ts +10 -7
  89. package/packages/skills/dist/loader.d.ts.map +1 -1
  90. package/packages/skills/dist/loader.js +15 -19
  91. package/packages/skills/dist/loader.js.map +1 -1
  92. package/packages/skills/dist/vector-schema.d.ts +89 -4
  93. package/packages/skills/dist/vector-schema.d.ts.map +1 -1
  94. package/packages/skills/dist/vector-schema.js +62 -4
  95. package/packages/skills/dist/vector-schema.js.map +1 -1
  96. package/packages/skills/package.json +1 -1
  97. package/packages/skills/src/loader.ts +27 -24
  98. package/packages/skills/src/vector-schema.ts +117 -4
  99. package/packages/skills/test/vector-schema.test.ts +167 -0
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/tooling-preference/package.json +1 -1
  112. package/packages/trajectory/package.json +1 -1
  113. package/packages/tui/package.json +1 -1
  114. package/packages/two-phase-review/package.json +1 -1
  115. package/packages/types/package.json +1 -1
  116. package/packages/voice-api/package.json +1 -1
  117. package/packages/webmcp-tester/package.json +1 -1
  118. package/packages/wiki/package.json +1 -1
  119. package/packages/workflow-events/package.json +1 -1
  120. package/packages/workspace-scanner/package.json +1 -1
  121. package/packages/worktree/package.json +1 -1
  122. package/packages/write-review/package.json +1 -1
  123. package/scripts/sync-to-qdrant.ts +19 -13
@@ -36,12 +36,70 @@ export const LEGACY_SKILL_COLLECTION_MODEL = "sentence-transformers/all-MiniLM-L
36
36
  /** The vector size of the legacy collection. */
37
37
  export const LEGACY_SKILL_COLLECTION_DIMENSIONS = 384;
38
38
  /**
39
- * Why the collection does not match the expected schema - or null when it
40
- * does.
39
+ * The body of a payload is truncated.
41
40
  *
42
- * The message names the numbers, so the reader can tell WHICH schema the
43
- * collection was built with.
41
+ * A payload is an index, not a file store - the complete skill comes from
42
+ * the skill service.
44
43
  */
44
+ export const SKILL_PAYLOAD_BODY_LIMIT = 2000;
45
+ /** The text of a payload is truncated as well. */
46
+ export const SKILL_PAYLOAD_TEXT_LIMIT = 1500;
47
+ /**
48
+ * A stable hash of a string.
49
+ *
50
+ * The same string always produces the same number, in every process and
51
+ * on every machine - a point id has to survive a restart.
52
+ *
53
+ * Two uses:
54
+ * - the id of a point is the hash of the name its payload carries, so the
55
+ * point id and the payload name always belong together. Two writers
56
+ * that store the same name address the same point.
57
+ * - the hash of the text of a skill says whether its content changed.
58
+ */
59
+ export function stableHash(value) {
60
+ let hash = 0;
61
+ for (let i = 0; i < value.length; i++) {
62
+ hash = (Math.imul(31, hash) + value.charCodeAt(i)) | 0;
63
+ }
64
+ return Math.abs(hash);
65
+ }
66
+ /**
67
+ * The text a skill vector is built from.
68
+ *
69
+ * The title is added when it differs from the name: a skill whose
70
+ * directory is named differently from its title carries both.
71
+ */
72
+ export function skillPointText(input) {
73
+ const parts = [input.name];
74
+ if (input.title && input.title !== input.name)
75
+ parts.push(input.title);
76
+ parts.push(input.description);
77
+ parts.push(input.body.slice(0, SKILL_PAYLOAD_TEXT_LIMIT));
78
+ return parts.join(" ");
79
+ }
80
+ /**
81
+ * The point of a skill.
82
+ *
83
+ * The runtime registry and `scripts/sync-to-qdrant.ts` both build their
84
+ * points here, so they cannot drift apart. A writer that used its own id
85
+ * would leave a second point for every skill the other one indexes, and
86
+ * an id that happens to collide with a foreign one would overwrite an
87
+ * unrelated skill.
88
+ */
89
+ export function skillQdrantPoint(payload) {
90
+ return { id: stableHash(payload.name), payload };
91
+ }
92
+ /**
93
+ * The payload of a skill point.
94
+ */
95
+ export function skillPointPayload(input) {
96
+ return {
97
+ name: input.name,
98
+ description: input.description,
99
+ body: input.body.slice(0, SKILL_PAYLOAD_BODY_LIMIT),
100
+ text: skillPointText(input),
101
+ };
102
+ }
45
103
  export function describeSkillSchemaMismatch(actual, expected) {
46
104
  if (actual.dimensions !== undefined && actual.dimensions !== expected.dimensions) {
47
105
  const model = expected.model ? ` (${expected.model})` : "";
@@ -1 +1 @@
1
- {"version":3,"file":"vector-schema.js","sourceRoot":"","sources":["../src/vector-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,6BAA6B,GAAG,wCAAwC,CAAC;AAEtF,gDAAgD;AAChD,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAAG,CAAC;AAYtD;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAC1C,MAAkD,EAClD,QAA2B;IAE3B,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC;QAClF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,OAAO,wBAAwB,MAAM,CAAC,UAAU,iDAAiD,QAAQ,CAAC,UAAU,GAAG,KAAK,EAAE,CAAC;IAChI,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC5E,OAAO,2BAA2B,MAAM,CAAC,QAAQ,kCAAkC,QAAQ,CAAC,QAAQ,EAAE,CAAC;IACxG,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"vector-schema.js","sourceRoot":"","sources":["../src/vector-schema.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/skills",
3
- "version": "1.1.413",
3
+ "version": "1.1.414",
4
4
  "description": "File-based skill system with pi.dev compatibility and Hermes self-improvement",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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 {
@@ -149,7 +157,7 @@ const RUNTIME_VECTOR_DISTANCE = "Cosine";
149
157
  export interface SkillVectorPoint {
150
158
  id: number;
151
159
  vector: number[];
152
- payload: QdrantSkillPoint;
160
+ payload: SkillPointPayload;
153
161
  }
154
162
 
155
163
  /**
@@ -613,13 +621,17 @@ export class SkillRegistry {
613
621
 
614
622
  /** The point of a skill in the vector index. */
615
623
  private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
616
- return {
617
- 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({
618
629
  name: skill.id,
630
+ title: skill.frontmatter.name,
619
631
  description: skill.frontmatter.description,
620
- body: skill.body.slice(0, 2000),
621
- text: [skill.id, skill.frontmatter.name, skill.frontmatter.description, skill.body.slice(0, 1500)].join(" "),
622
- };
632
+ body: skill.body,
633
+ });
634
+ return skillQdrantPoint(payload);
623
635
  }
624
636
 
625
637
  /**
@@ -677,7 +689,7 @@ export class SkillRegistry {
677
689
  */
678
690
  private _scheduleIndex(skill: Skill): void {
679
691
  const point = this._toQdrantPoint(skill);
680
- const hash = this._hashId(point.text);
692
+ const hash = stableHash(point.payload.text);
681
693
  if (this._indexedHashes.get(skill.id) === hash) return;
682
694
 
683
695
  this._indexedHashes.set(skill.id, hash);
@@ -729,12 +741,12 @@ export class SkillRegistry {
729
741
  while (this._indexQueue.length > 0) {
730
742
  const batch = this._indexQueue.splice(0, this._batchSize);
731
743
  try {
732
- const vectors = await indexer.embed(batch.map((item) => item.point.text));
744
+ const vectors = await indexer.embed(batch.map((item) => item.point.payload.text));
733
745
  const points: SkillVectorPoint[] = [];
734
746
  batch.forEach((item, position) => {
735
747
  const vector = vectors[position];
736
748
  if (!vector || vector.length === 0) return;
737
- points.push({ id: item.point.id, vector, payload: item.point });
749
+ points.push({ id: item.point.id, vector, payload: item.point.payload });
738
750
  });
739
751
  if (points.length > 0) await indexer.upsert(points);
740
752
  } catch {
@@ -749,15 +761,6 @@ export class SkillRegistry {
749
761
  }
750
762
  }
751
763
 
752
- /** Simple deterministic ID for Qdrant (no bigints needed). */
753
- private _hashId(id: string): number {
754
- let h = 0;
755
- for (let i = 0; i < id.length; i++) {
756
- h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
757
- }
758
- return Math.abs(h);
759
- }
760
-
761
764
  /**
762
765
  * Register a skill (auto-indexes to Qdrant if configured).
763
766
  */
@@ -1084,12 +1087,12 @@ export class SkillRegistry {
1084
1087
  const batch = skills.slice(i, i + this._batchSize);
1085
1088
  const points = batch.map((skill) => this._toQdrantPoint(skill));
1086
1089
  try {
1087
- const vectors = await indexer.embed(points.map((point) => point.text));
1090
+ const vectors = await indexer.embed(points.map((point) => point.payload.text));
1088
1091
  const payload: SkillVectorPoint[] = [];
1089
1092
  points.forEach((point, position) => {
1090
1093
  const vector = vectors[position];
1091
1094
  if (!vector || vector.length === 0) return;
1092
- payload.push({ id: point.id, vector, payload: point });
1095
+ payload.push({ id: point.id, vector, payload: point.payload });
1093
1096
  });
1094
1097
  if (payload.length > 0) await indexer.upsert(payload);
1095
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,
@@ -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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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.413",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/trajectory",
3
- "version": "1.1.413",
3
+ "version": "1.1.414",
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.413",
3
+ "version": "1.1.414",
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.413",
3
+ "version": "1.1.414",
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.413",
3
+ "version": "1.1.414",
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.413",
3
+ "version": "1.1.414",
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.413",
3
+ "version": "1.1.414",
4
4
  "description": "WebMCP-powered E2E testing for pi-harness - natural language test scenarios",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",