@soleri/core 9.4.0 → 9.6.0

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 (228) hide show
  1. package/dist/adapters/claude-code-adapter.d.ts +27 -0
  2. package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
  3. package/dist/adapters/claude-code-adapter.js +111 -0
  4. package/dist/adapters/claude-code-adapter.js.map +1 -0
  5. package/dist/adapters/index.d.ts +9 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/registry.d.ts +21 -0
  10. package/dist/adapters/registry.d.ts.map +1 -0
  11. package/dist/adapters/registry.js +44 -0
  12. package/dist/adapters/registry.js.map +1 -0
  13. package/dist/adapters/types.d.ts +93 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +10 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/brain/brain.d.ts +12 -1
  18. package/dist/brain/brain.d.ts.map +1 -1
  19. package/dist/brain/brain.js +106 -44
  20. package/dist/brain/brain.js.map +1 -1
  21. package/dist/brain/intelligence.d.ts.map +1 -1
  22. package/dist/brain/intelligence.js +36 -30
  23. package/dist/brain/intelligence.js.map +1 -1
  24. package/dist/chat/agent-loop.js +1 -1
  25. package/dist/chat/agent-loop.js.map +1 -1
  26. package/dist/chat/notifications.d.ts.map +1 -1
  27. package/dist/chat/notifications.js +4 -0
  28. package/dist/chat/notifications.js.map +1 -1
  29. package/dist/control/intent-router.d.ts +1 -0
  30. package/dist/control/intent-router.d.ts.map +1 -1
  31. package/dist/control/intent-router.js +11 -5
  32. package/dist/control/intent-router.js.map +1 -1
  33. package/dist/curator/curator.d.ts +4 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +141 -27
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/hooks/candidate-scorer.d.ts +28 -0
  38. package/dist/hooks/candidate-scorer.d.ts.map +1 -0
  39. package/dist/hooks/candidate-scorer.js +20 -0
  40. package/dist/hooks/candidate-scorer.js.map +1 -0
  41. package/dist/hooks/index.d.ts +2 -0
  42. package/dist/hooks/index.d.ts.map +1 -0
  43. package/dist/hooks/index.js +2 -0
  44. package/dist/hooks/index.js.map +1 -0
  45. package/dist/index.d.ts +14 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +12 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/llm/llm-client.d.ts.map +1 -1
  50. package/dist/llm/llm-client.js +1 -0
  51. package/dist/llm/llm-client.js.map +1 -1
  52. package/dist/packs/index.d.ts +3 -2
  53. package/dist/packs/index.d.ts.map +1 -1
  54. package/dist/packs/index.js +3 -2
  55. package/dist/packs/index.js.map +1 -1
  56. package/dist/packs/lockfile.d.ts +23 -1
  57. package/dist/packs/lockfile.d.ts.map +1 -1
  58. package/dist/packs/lockfile.js +50 -4
  59. package/dist/packs/lockfile.js.map +1 -1
  60. package/dist/packs/pack-installer.d.ts +10 -0
  61. package/dist/packs/pack-installer.d.ts.map +1 -1
  62. package/dist/packs/pack-installer.js +69 -2
  63. package/dist/packs/pack-installer.js.map +1 -1
  64. package/dist/packs/pack-lifecycle.d.ts +50 -0
  65. package/dist/packs/pack-lifecycle.d.ts.map +1 -0
  66. package/dist/packs/pack-lifecycle.js +91 -0
  67. package/dist/packs/pack-lifecycle.js.map +1 -0
  68. package/dist/packs/types.d.ts +64 -44
  69. package/dist/packs/types.d.ts.map +1 -1
  70. package/dist/packs/types.js +9 -0
  71. package/dist/packs/types.js.map +1 -1
  72. package/dist/persistence/sqlite-provider.d.ts +5 -1
  73. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  74. package/dist/persistence/sqlite-provider.js +22 -2
  75. package/dist/persistence/sqlite-provider.js.map +1 -1
  76. package/dist/planning/github-projection.d.ts +8 -8
  77. package/dist/planning/github-projection.d.ts.map +1 -1
  78. package/dist/planning/github-projection.js +42 -42
  79. package/dist/planning/github-projection.js.map +1 -1
  80. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  81. package/dist/planning/plan-lifecycle.js +6 -1
  82. package/dist/planning/plan-lifecycle.js.map +1 -1
  83. package/dist/plugins/types.d.ts +21 -21
  84. package/dist/queue/pipeline-runner.d.ts.map +1 -1
  85. package/dist/queue/pipeline-runner.js +4 -0
  86. package/dist/queue/pipeline-runner.js.map +1 -1
  87. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  88. package/dist/runtime/curator-extra-ops.js +9 -1
  89. package/dist/runtime/curator-extra-ops.js.map +1 -1
  90. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  91. package/dist/runtime/facades/memory-facade.js +169 -0
  92. package/dist/runtime/facades/memory-facade.js.map +1 -1
  93. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  94. package/dist/runtime/orchestrate-ops.js +133 -4
  95. package/dist/runtime/orchestrate-ops.js.map +1 -1
  96. package/dist/runtime/runtime.d.ts.map +1 -1
  97. package/dist/runtime/runtime.js +128 -90
  98. package/dist/runtime/runtime.js.map +1 -1
  99. package/dist/runtime/session-briefing.d.ts.map +1 -1
  100. package/dist/runtime/session-briefing.js +44 -11
  101. package/dist/runtime/session-briefing.js.map +1 -1
  102. package/dist/runtime/shutdown-registry.d.ts +36 -0
  103. package/dist/runtime/shutdown-registry.d.ts.map +1 -0
  104. package/dist/runtime/shutdown-registry.js +74 -0
  105. package/dist/runtime/shutdown-registry.js.map +1 -0
  106. package/dist/runtime/types.d.ts +10 -1
  107. package/dist/runtime/types.d.ts.map +1 -1
  108. package/dist/subagent/concurrency-manager.d.ts +29 -0
  109. package/dist/subagent/concurrency-manager.d.ts.map +1 -0
  110. package/dist/subagent/concurrency-manager.js +73 -0
  111. package/dist/subagent/concurrency-manager.js.map +1 -0
  112. package/dist/subagent/dispatcher.d.ts +41 -0
  113. package/dist/subagent/dispatcher.d.ts.map +1 -0
  114. package/dist/subagent/dispatcher.js +259 -0
  115. package/dist/subagent/dispatcher.js.map +1 -0
  116. package/dist/subagent/index.d.ts +14 -0
  117. package/dist/subagent/index.d.ts.map +1 -0
  118. package/dist/subagent/index.js +15 -0
  119. package/dist/subagent/index.js.map +1 -0
  120. package/dist/subagent/orphan-reaper.d.ts +37 -0
  121. package/dist/subagent/orphan-reaper.d.ts.map +1 -0
  122. package/dist/subagent/orphan-reaper.js +71 -0
  123. package/dist/subagent/orphan-reaper.js.map +1 -0
  124. package/dist/subagent/result-aggregator.d.ts +7 -0
  125. package/dist/subagent/result-aggregator.d.ts.map +1 -0
  126. package/dist/subagent/result-aggregator.js +57 -0
  127. package/dist/subagent/result-aggregator.js.map +1 -0
  128. package/dist/subagent/task-checkout.d.ts +36 -0
  129. package/dist/subagent/task-checkout.d.ts.map +1 -0
  130. package/dist/subagent/task-checkout.js +52 -0
  131. package/dist/subagent/task-checkout.js.map +1 -0
  132. package/dist/subagent/types.d.ts +114 -0
  133. package/dist/subagent/types.d.ts.map +1 -0
  134. package/dist/subagent/types.js +9 -0
  135. package/dist/subagent/types.js.map +1 -0
  136. package/dist/subagent/workspace-resolver.d.ts +35 -0
  137. package/dist/subagent/workspace-resolver.d.ts.map +1 -0
  138. package/dist/subagent/workspace-resolver.js +99 -0
  139. package/dist/subagent/workspace-resolver.js.map +1 -0
  140. package/dist/transport/http-server.d.ts.map +1 -1
  141. package/dist/transport/http-server.js +49 -3
  142. package/dist/transport/http-server.js.map +1 -1
  143. package/dist/transport/ws-server.d.ts.map +1 -1
  144. package/dist/transport/ws-server.js +7 -0
  145. package/dist/transport/ws-server.js.map +1 -1
  146. package/dist/vault/linking.d.ts +3 -4
  147. package/dist/vault/linking.d.ts.map +1 -1
  148. package/dist/vault/linking.js +79 -32
  149. package/dist/vault/linking.js.map +1 -1
  150. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  151. package/dist/vault/vault-maintenance.js +7 -14
  152. package/dist/vault/vault-maintenance.js.map +1 -1
  153. package/dist/vault/vault-memories.d.ts.map +1 -1
  154. package/dist/vault/vault-memories.js +19 -9
  155. package/dist/vault/vault-memories.js.map +1 -1
  156. package/dist/vault/vault-schema.d.ts +1 -0
  157. package/dist/vault/vault-schema.d.ts.map +1 -1
  158. package/dist/vault/vault-schema.js +20 -0
  159. package/dist/vault/vault-schema.js.map +1 -1
  160. package/dist/vault/vault.d.ts.map +1 -1
  161. package/dist/vault/vault.js +7 -3
  162. package/dist/vault/vault.js.map +1 -1
  163. package/package.json +8 -2
  164. package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
  165. package/src/__tests__/adapters/registry.test.ts +100 -0
  166. package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
  167. package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
  168. package/src/__tests__/subagent/dispatcher.test.ts +195 -0
  169. package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
  170. package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
  171. package/src/__tests__/subagent/task-checkout.test.ts +86 -0
  172. package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
  173. package/src/adapters/claude-code-adapter.ts +163 -0
  174. package/src/adapters/index.ts +22 -0
  175. package/src/adapters/registry.ts +53 -0
  176. package/src/adapters/types.ts +114 -0
  177. package/src/brain/brain.ts +120 -46
  178. package/src/brain/intelligence.ts +42 -34
  179. package/src/chat/agent-loop.ts +1 -1
  180. package/src/chat/notifications.ts +4 -0
  181. package/src/control/intent-router.ts +10 -8
  182. package/src/curator/curator.ts +146 -29
  183. package/src/hooks/candidate-scorer.test.ts +76 -0
  184. package/src/hooks/candidate-scorer.ts +39 -0
  185. package/src/index.ts +40 -1
  186. package/src/llm/llm-client.ts +1 -0
  187. package/src/packs/index.ts +5 -1
  188. package/src/packs/lockfile.ts +70 -5
  189. package/src/packs/pack-installer.ts +78 -2
  190. package/src/packs/pack-lifecycle.ts +115 -0
  191. package/src/packs/pack-lockfile.test.ts +1 -1
  192. package/src/packs/pack-system.test.ts +1 -1
  193. package/src/packs/types.ts +40 -2
  194. package/src/persistence/sqlite-provider.ts +27 -2
  195. package/src/planning/github-projection.ts +48 -44
  196. package/src/planning/plan-lifecycle.ts +14 -1
  197. package/src/queue/pipeline-runner.ts +4 -0
  198. package/src/runtime/admin-setup-ops.test.ts +9 -4
  199. package/src/runtime/curator-extra-ops.test.ts +7 -0
  200. package/src/runtime/curator-extra-ops.ts +10 -1
  201. package/src/runtime/facades/curator-facade.test.ts +7 -0
  202. package/src/runtime/facades/memory-facade.ts +187 -0
  203. package/src/runtime/orchestrate-ops.ts +156 -4
  204. package/src/runtime/runtime.test.ts +50 -2
  205. package/src/runtime/runtime.ts +132 -89
  206. package/src/runtime/session-briefing.test.ts +94 -2
  207. package/src/runtime/session-briefing.ts +48 -12
  208. package/src/runtime/shutdown-registry.test.ts +151 -0
  209. package/src/runtime/shutdown-registry.ts +85 -0
  210. package/src/runtime/types.ts +10 -1
  211. package/src/subagent/concurrency-manager.ts +89 -0
  212. package/src/subagent/dispatcher.ts +326 -0
  213. package/src/subagent/index.ts +28 -0
  214. package/src/subagent/orphan-reaper.ts +82 -0
  215. package/src/subagent/result-aggregator.ts +66 -0
  216. package/src/subagent/task-checkout.ts +60 -0
  217. package/src/subagent/types.ts +138 -0
  218. package/src/subagent/workspace-resolver.ts +117 -0
  219. package/src/transport/http-server.ts +50 -3
  220. package/src/transport/ws-server.ts +8 -0
  221. package/src/vault/linking.test.ts +12 -0
  222. package/src/vault/linking.ts +90 -44
  223. package/src/vault/vault-maintenance.ts +11 -18
  224. package/src/vault/vault-memories.ts +21 -13
  225. package/src/vault/vault-scaling.test.ts +3 -2
  226. package/src/vault/vault-schema.ts +21 -0
  227. package/src/vault/vault.ts +8 -3
  228. package/vitest.config.ts +2 -0
@@ -5,6 +5,7 @@ import type {
5
5
  CuratorStatus,
6
6
  TagNormalizationResult,
7
7
  CanonicalTag,
8
+ DuplicateCandidate,
8
9
  DuplicateDetectionResult,
9
10
  Contradiction,
10
11
  ContradictionStatus,
@@ -17,14 +18,17 @@ import type {
17
18
  } from './types.js';
18
19
 
19
20
  import {
20
- detectDuplicates as detectDuplicatesPure,
21
21
  DEFAULT_DUPLICATE_THRESHOLD,
22
+ MERGE_SUGGESTION_THRESHOLD,
23
+ buildVocabulary,
24
+ entryToText,
22
25
  } from './duplicate-detector.js';
23
26
  import {
24
27
  findContradictions,
25
28
  DEFAULT_CONTRADICTION_THRESHOLD,
26
29
  type ContradictionCandidate,
27
30
  } from './contradiction-detector.js';
31
+ import { tokenize, calculateTfIdf, cosineSimilarity } from '../text/similarity.js';
28
32
  import {
29
33
  normalizeTag as normalizeTagPure,
30
34
  normalizeAndDedup,
@@ -40,6 +44,7 @@ import { enrichEntryMetadata } from './metadata-enricher.js';
40
44
  // ─── Constants ──────────────────────────────────────────────────────
41
45
 
42
46
  const DEFAULT_STALE_DAYS = 90;
47
+ const DEFAULT_BATCH_SIZE = 100;
43
48
 
44
49
  // ─── Curator Class ──────────────────────────────────────────────────
45
50
 
@@ -151,19 +156,101 @@ export class Curator {
151
156
  // ─── Duplicates (delegates to duplicate-detector) ─────────────
152
157
 
153
158
  detectDuplicates(entryId?: string, threshold?: number): DuplicateDetectionResult[] {
154
- const results = detectDuplicatesPure(this.vault.list({ limit: 100000 }), entryId, threshold);
155
- // Filter out dismissed pairs
159
+ const effectiveThreshold = threshold ?? DEFAULT_DUPLICATE_THRESHOLD;
156
160
  const dismissed = this.getDismissedPairs();
157
- if (dismissed.size === 0) return results;
158
- return results
159
- .map((r) => ({
160
- ...r,
161
- matches: r.matches.filter((m) => {
162
- const key = [r.entryId, m.entryId].sort().join('::');
163
- return !dismissed.has(key);
164
- }),
165
- }))
166
- .filter((r) => r.matches.length > 0);
161
+
162
+ // --- Phase 1: Content-hash exact duplicates (O(n) via GROUP BY) ---
163
+ const exactDupes = this.provider.all<{ content_hash: string; ids: string }>(
164
+ `SELECT content_hash, GROUP_CONCAT(id) as ids FROM entries
165
+ WHERE content_hash IS NOT NULL
166
+ GROUP BY content_hash HAVING COUNT(*) > 1`,
167
+ );
168
+
169
+ const results: DuplicateDetectionResult[] = [];
170
+ const seenPairs = new Set<string>();
171
+
172
+ for (const { ids } of exactDupes) {
173
+ const idList = ids.split(',');
174
+ if (entryId && !idList.includes(entryId)) continue;
175
+ const targets = entryId ? [entryId] : idList;
176
+ for (const targetId of targets) {
177
+ const matches: DuplicateCandidate[] = [];
178
+ for (const otherId of idList) {
179
+ if (otherId === targetId) continue;
180
+ const pairKey = [targetId, otherId].sort().join('::');
181
+ if (dismissed.has(pairKey) || seenPairs.has(pairKey)) continue;
182
+ seenPairs.add(pairKey);
183
+ const other = this.vault.get(otherId);
184
+ if (!other) continue;
185
+ matches.push({
186
+ entryId: otherId,
187
+ title: other.title,
188
+ similarity: 1.0,
189
+ suggestMerge: true,
190
+ });
191
+ }
192
+ if (matches.length > 0) {
193
+ results.push({ entryId: targetId, matches, scannedCount: idList.length - 1 });
194
+ }
195
+ }
196
+ }
197
+
198
+ // --- Phase 2: FTS5 fuzzy candidate matching ---
199
+ // For each entry, use FTS5 MATCH to find top candidates, then TF-IDF cosine similarity.
200
+ // Complexity is O(n * k) where k = FTS5 candidate limit (10), not O(n^2).
201
+ const exactDupeEntryIds = new Set(results.map((r) => r.entryId));
202
+ const targetEntries = entryId
203
+ ? ([this.vault.get(entryId)].filter(Boolean) as IntelligenceEntry[])
204
+ : this.listBatched();
205
+
206
+ // Build vocabulary from all entries (batched)
207
+ const allEntries = entryId ? this.listBatched() : targetEntries;
208
+ const vocabulary = buildVocabulary(allEntries);
209
+
210
+ for (const entry of targetEntries) {
211
+ // Skip entries already fully handled by exact-hash matches
212
+ if (exactDupeEntryIds.has(entry.id)) continue;
213
+
214
+ // FTS5 candidate retrieval: find top-10 similar entries
215
+ let candidates: IntelligenceEntry[];
216
+ try {
217
+ candidates = this.vault
218
+ .search(entry.title, { domain: entry.domain, limit: 10 })
219
+ .map((r) => r.entry)
220
+ .filter((c) => c.id !== entry.id);
221
+ } catch {
222
+ candidates = [];
223
+ }
224
+ if (candidates.length === 0) continue;
225
+
226
+ const entryVec = calculateTfIdf(tokenize(entryToText(entry)), vocabulary);
227
+ const matches: DuplicateCandidate[] = [];
228
+
229
+ for (const candidate of candidates) {
230
+ // Skip cross-domain pairs
231
+ if (entry.domain !== candidate.domain) continue;
232
+ const pairKey = [entry.id, candidate.id].sort().join('::');
233
+ if (dismissed.has(pairKey)) continue;
234
+
235
+ const candidateVec = calculateTfIdf(tokenize(entryToText(candidate)), vocabulary);
236
+ const similarity = cosineSimilarity(entryVec, candidateVec);
237
+ if (similarity >= effectiveThreshold) {
238
+ matches.push({
239
+ entryId: candidate.id,
240
+ title: candidate.title,
241
+ similarity,
242
+ suggestMerge: similarity >= MERGE_SUGGESTION_THRESHOLD,
243
+ });
244
+ }
245
+ }
246
+
247
+ if (matches.length > 0) {
248
+ matches.sort((a, b) => b.similarity - a.similarity);
249
+ results.push({ entryId: entry.id, matches, scannedCount: candidates.length });
250
+ }
251
+ }
252
+
253
+ return results;
167
254
  }
168
255
 
169
256
  dismissDuplicate(entryIdA: string, entryIdB: string, reason?: string): { dismissed: boolean } {
@@ -187,9 +274,11 @@ export class Curator {
187
274
  detectContradictions(threshold?: number): Contradiction[] {
188
275
  const searchFn = (title: string) =>
189
276
  this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
190
- return this.persistContradictions(
191
- findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
192
- );
277
+ // Load only anti-patterns and patterns (bounded by type), not the entire vault
278
+ const antipatterns = this.vault.list({ type: 'anti-pattern', limit: 10000 });
279
+ const patterns = this.vault.list({ type: 'pattern', limit: 10000 });
280
+ const entries = [...antipatterns, ...patterns];
281
+ return this.persistContradictions(findContradictions(entries, threshold, searchFn));
193
282
  }
194
283
 
195
284
  getContradictions(status?: ContradictionStatus): Contradiction[] {
@@ -218,10 +307,12 @@ export class Curator {
218
307
  ): Promise<{ contradictions: Contradiction[]; method: 'tfidf-only' }> {
219
308
  const searchFn = (title: string) =>
220
309
  this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
310
+ // Load only anti-patterns and patterns (bounded by type), not the entire vault
311
+ const antipatterns = this.vault.list({ type: 'anti-pattern', limit: 10000 });
312
+ const patterns = this.vault.list({ type: 'pattern', limit: 10000 });
313
+ const entries = [...antipatterns, ...patterns];
221
314
  return {
222
- contradictions: this.persistContradictions(
223
- findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
224
- ),
315
+ contradictions: this.persistContradictions(findContradictions(entries, threshold, searchFn)),
225
316
  method: 'tfidf-only',
226
317
  };
227
318
  }
@@ -249,19 +340,28 @@ export class Curator {
249
340
 
250
341
  groomAll(): GroomAllResult {
251
342
  const start = Date.now();
252
- const entries = this.vault.list({ limit: 100000 });
253
343
  let tagsNormalized = 0,
254
- staleCount = 0;
255
- for (const entry of entries) {
256
- const result = this.groomEntry(entry.id);
257
- if (result) {
258
- tagsNormalized += result.tagsNormalized.filter((t) => t.wasAliased).length;
259
- if (result.stale) staleCount++;
344
+ staleCount = 0,
345
+ totalEntries = 0;
346
+ // Batch pagination — process entries in batches instead of loading all at once
347
+ let offset = 0;
348
+ while (true) {
349
+ const batch = this.vault.list({ limit: DEFAULT_BATCH_SIZE, offset });
350
+ if (batch.length === 0) break;
351
+ totalEntries += batch.length;
352
+ for (const entry of batch) {
353
+ const result = this.groomEntry(entry.id);
354
+ if (result) {
355
+ tagsNormalized += result.tagsNormalized.filter((t) => t.wasAliased).length;
356
+ if (result.stale) staleCount++;
357
+ }
260
358
  }
359
+ if (batch.length < DEFAULT_BATCH_SIZE) break;
360
+ offset += DEFAULT_BATCH_SIZE;
261
361
  }
262
362
  return {
263
- totalEntries: entries.length,
264
- groomedCount: entries.length,
363
+ totalEntries,
364
+ groomedCount: totalEntries,
265
365
  tagsNormalized,
266
366
  staleCount,
267
367
  durationMs: Date.now() - start,
@@ -343,7 +443,8 @@ export class Curator {
343
443
  // ─── Health Audit (delegates to health-audit) ─────────────────
344
444
 
345
445
  healthAudit(): HealthAuditResult {
346
- const entries = this.vault.list({ limit: 100000 });
446
+ // Load entries in batches instead of all at once
447
+ const entries = this.listBatched();
347
448
  const dataProvider: HealthDataProvider = {
348
449
  getStaleCount: (threshold) =>
349
450
  (
@@ -480,6 +581,22 @@ export class Curator {
480
581
 
481
582
  // ─── Private Helpers ──────────────────────────────────────────
482
583
 
584
+ /**
585
+ * Load all vault entries using batched pagination instead of a single 100k query.
586
+ */
587
+ private listBatched(batchSize: number = DEFAULT_BATCH_SIZE): IntelligenceEntry[] {
588
+ const all: IntelligenceEntry[] = [];
589
+ let offset = 0;
590
+ while (true) {
591
+ const batch = this.vault.list({ limit: batchSize, offset });
592
+ if (batch.length === 0) break;
593
+ all.push(...batch);
594
+ if (batch.length < batchSize) break;
595
+ offset += batchSize;
596
+ }
597
+ return all;
598
+ }
599
+
483
600
  private persistContradictions(candidates: ContradictionCandidate[]): Contradiction[] {
484
601
  const detected: Contradiction[] = [];
485
602
  for (const c of candidates) {
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { scoreCandidateForConversion } from './candidate-scorer.js';
3
+ import type { CandidateDimensions } from './candidate-scorer.js';
4
+
5
+ describe('scoreCandidateForConversion', () => {
6
+ it('should identify strong candidate (4/4 HIGH)', () => {
7
+ const dimensions: CandidateDimensions = {
8
+ frequency: 'HIGH',
9
+ eventCorrelation: 'HIGH',
10
+ determinism: 'HIGH',
11
+ autonomy: 'HIGH',
12
+ };
13
+ const result = scoreCandidateForConversion(dimensions);
14
+ expect(result.highCount).toBe(4);
15
+ expect(result.candidate).toBe(true);
16
+ });
17
+
18
+ it('should identify borderline candidate (3/4 HIGH)', () => {
19
+ const dimensions: CandidateDimensions = {
20
+ frequency: 'HIGH',
21
+ eventCorrelation: 'HIGH',
22
+ determinism: 'HIGH',
23
+ autonomy: 'LOW',
24
+ };
25
+ const result = scoreCandidateForConversion(dimensions);
26
+ expect(result.highCount).toBe(3);
27
+ expect(result.candidate).toBe(true);
28
+ });
29
+
30
+ it('should reject non-candidate (2/4 HIGH)', () => {
31
+ const dimensions: CandidateDimensions = {
32
+ frequency: 'HIGH',
33
+ eventCorrelation: 'LOW',
34
+ determinism: 'HIGH',
35
+ autonomy: 'LOW',
36
+ };
37
+ const result = scoreCandidateForConversion(dimensions);
38
+ expect(result.highCount).toBe(2);
39
+ expect(result.candidate).toBe(false);
40
+ });
41
+
42
+ it('should reject weak candidate (1/4 HIGH)', () => {
43
+ const dimensions: CandidateDimensions = {
44
+ frequency: 'LOW',
45
+ eventCorrelation: 'LOW',
46
+ determinism: 'LOW',
47
+ autonomy: 'HIGH',
48
+ };
49
+ const result = scoreCandidateForConversion(dimensions);
50
+ expect(result.highCount).toBe(1);
51
+ expect(result.candidate).toBe(false);
52
+ });
53
+
54
+ it('should reject zero score (0/4 HIGH)', () => {
55
+ const dimensions: CandidateDimensions = {
56
+ frequency: 'LOW',
57
+ eventCorrelation: 'LOW',
58
+ determinism: 'LOW',
59
+ autonomy: 'LOW',
60
+ };
61
+ const result = scoreCandidateForConversion(dimensions);
62
+ expect(result.highCount).toBe(0);
63
+ expect(result.candidate).toBe(false);
64
+ });
65
+
66
+ it('should preserve dimension values in result', () => {
67
+ const dimensions: CandidateDimensions = {
68
+ frequency: 'HIGH',
69
+ eventCorrelation: 'LOW',
70
+ determinism: 'HIGH',
71
+ autonomy: 'HIGH',
72
+ };
73
+ const result = scoreCandidateForConversion(dimensions);
74
+ expect(result.dimensions).toEqual(dimensions);
75
+ });
76
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Candidate scoring rubric for skill-to-hook conversion.
3
+ * 4-dimension scoring: frequency, eventCorrelation, determinism, autonomy.
4
+ * Threshold: >= 3 HIGH dimensions = candidate for conversion.
5
+ */
6
+
7
+ export type DimensionLevel = 'HIGH' | 'LOW';
8
+
9
+ export interface CandidateDimensions {
10
+ /** 3+ manual calls per session for same event type → HIGH */
11
+ frequency: DimensionLevel;
12
+ /** Skill consistently triggers on a recognizable hook event → HIGH */
13
+ eventCorrelation: DimensionLevel;
14
+ /** Skill produces consistent, non-exploratory guidance → HIGH */
15
+ determinism: DimensionLevel;
16
+ /** Skill requires no interactive user decisions mid-execution → HIGH */
17
+ autonomy: DimensionLevel;
18
+ }
19
+
20
+ export interface CandidateScore {
21
+ dimensions: CandidateDimensions;
22
+ highCount: number;
23
+ candidate: boolean;
24
+ }
25
+
26
+ /**
27
+ * Score a skill for hook conversion candidacy.
28
+ * @param dimensions - The 4 scored dimensions (each HIGH or LOW)
29
+ * @returns Score with candidate boolean (true if >= 3 HIGH)
30
+ */
31
+ export function scoreCandidateForConversion(dimensions: CandidateDimensions): CandidateScore {
32
+ const values = Object.values(dimensions);
33
+ const highCount = values.filter((v) => v === 'HIGH').length;
34
+ return {
35
+ dimensions,
36
+ highCount,
37
+ candidate: highCount >= 3,
38
+ };
39
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,34 @@
1
+ // ─── Adapters ────────────────────────────────────────────────────────
2
+ export { RuntimeAdapterRegistry } from './adapters/registry.js';
3
+ export { ClaudeCodeRuntimeAdapter } from './adapters/claude-code-adapter.js';
4
+ export type {
5
+ RuntimeAdapter,
6
+ AdapterExecutionContext,
7
+ AdapterExecutionResult,
8
+ AdapterTokenUsage,
9
+ AdapterSessionState,
10
+ AdapterSessionCodec,
11
+ AdapterEnvironmentTestResult,
12
+ } from './adapters/types.js';
13
+
14
+ // ─── Subagent ────────────────────────────────────────────────────────
15
+ export { SubagentDispatcher } from './subagent/dispatcher.js';
16
+ export { TaskCheckout } from './subagent/task-checkout.js';
17
+ export { WorkspaceResolver } from './subagent/workspace-resolver.js';
18
+ export { ConcurrencyManager } from './subagent/concurrency-manager.js';
19
+ export { OrphanReaper } from './subagent/orphan-reaper.js';
20
+ export { aggregate as aggregateResults } from './subagent/result-aggregator.js';
21
+ export type {
22
+ SubagentTask,
23
+ SubagentStatus,
24
+ SubagentResult,
25
+ DispatchOptions,
26
+ AggregatedResult,
27
+ ClaimInfo,
28
+ WorktreeInfo,
29
+ TrackedProcess,
30
+ } from './subagent/types.js';
31
+
1
32
  // ─── Paths ──────────────────────────────────────────────────────────
2
33
  export {
3
34
  SOLERI_HOME,
@@ -551,6 +582,8 @@ export { createSemanticFacades } from './runtime/facades/index.js';
551
582
  export { createDomainFacade, createDomainFacades } from './runtime/domain-ops.js';
552
583
  export { FeatureFlags } from './runtime/feature-flags.js';
553
584
  export type { FlagDefinition } from './runtime/feature-flags.js';
585
+ export { ShutdownRegistry } from './runtime/shutdown-registry.js';
586
+ export type { ShutdownCallback } from './runtime/shutdown-registry.js';
554
587
  export type { AgentRuntimeConfig, AgentRuntime } from './runtime/types.js';
555
588
  export {
556
589
  deprecationWarning,
@@ -615,7 +648,13 @@ export { computeContentHash } from './vault/content-hash.js';
615
648
  export type { HashableEntry } from './vault/content-hash.js';
616
649
 
617
650
  // ─── Knowledge Packs ────────────────────────────────────────────────────
618
- export { PackInstaller, packManifestSchema } from './packs/index.js';
651
+ export {
652
+ PackInstaller,
653
+ PackLifecycleManager,
654
+ packManifestSchema,
655
+ VALID_TRANSITIONS,
656
+ } from './packs/index.js';
657
+ export type { PackState, PackTransition } from './packs/index.js';
619
658
  export { PackLockfile, inferPackType } from './packs/index.js';
620
659
  export { resolvePack, checkNpmVersion, checkVersionCompat } from './packs/index.js';
621
660
  export type {
@@ -237,6 +237,7 @@ export class LLMClient {
237
237
  temperature: options.temperature ?? 0.3,
238
238
  max_completion_tokens: options.maxTokens ?? 500,
239
239
  }),
240
+ signal: AbortSignal.timeout(60_000),
240
241
  });
241
242
 
242
243
  if (response.headers) {
@@ -5,17 +5,21 @@
5
5
  export {
6
6
  packManifestSchema,
7
7
  PACK_TIERS,
8
+ VALID_TRANSITIONS,
8
9
  type PackManifest,
9
10
  type PackTier as ManifestPackTier,
10
11
  type PackStatus,
12
+ type PackState,
13
+ type PackTransition,
11
14
  type InstalledPack,
12
15
  type InstallResult,
13
16
  type ValidateResult,
14
17
  } from './types.js';
15
18
 
16
19
  export { PackInstaller } from './pack-installer.js';
20
+ export { PackLifecycleManager } from './pack-lifecycle.js';
17
21
 
18
- export { PackLockfile, inferPackType } from './lockfile.js';
22
+ export { PackLockfile, inferPackType, LOCKFILE_VERSION } from './lockfile.js';
19
23
  export type { LockEntry, PackType, PackSource, PackTier, LockfileData } from './lockfile.js';
20
24
 
21
25
  export { resolvePack, checkNpmVersion, checkVersionCompat } from './resolver.js';
@@ -41,15 +41,25 @@ export interface LockEntry {
41
41
  soleriRange?: string;
42
42
  /** Pack tier: default (ships with engine), community (free), premium (unlocked today) */
43
43
  tier?: PackTier;
44
+ /** Current pack lifecycle state */
45
+ state?: string;
46
+ /** Most recent lifecycle transition */
47
+ lastTransition?: { from: string; to: string; timestamp: string; reason?: string };
48
+ /** ISO timestamp when the pack was disabled */
49
+ disabledAt?: string;
50
+ /** Error details if the pack is in an error state */
51
+ errorMessage?: string;
44
52
  }
45
53
 
46
54
  export type PackType = 'hooks' | 'skills' | 'knowledge' | 'domain' | 'bundle';
47
55
  export type PackSource = 'built-in' | 'local' | 'npm';
48
56
  export type PackTier = 'default' | 'community' | 'premium';
49
57
 
58
+ export const LOCKFILE_VERSION = 2;
59
+
50
60
  export interface LockfileData {
51
61
  /** Lockfile format version */
52
- version: 1;
62
+ version: number;
53
63
  /** Map of packId → lock entry */
54
64
  packs: Record<string, LockEntry>;
55
65
  }
@@ -141,18 +151,73 @@ export class PackLockfile {
141
151
  return 'sha256-' + createHash('sha256').update(content).digest('hex');
142
152
  }
143
153
 
154
+ /**
155
+ * Update lifecycle fields for a pack entry and save.
156
+ */
157
+ updateLifecycle(
158
+ packId: string,
159
+ state: string,
160
+ transition?: { from: string; to: string; reason?: string },
161
+ ): void {
162
+ const entry = this.data.packs[packId];
163
+ if (!entry) return;
164
+
165
+ entry.state = state;
166
+
167
+ if (transition) {
168
+ entry.lastTransition = {
169
+ from: transition.from,
170
+ to: transition.to,
171
+ timestamp: new Date().toISOString(),
172
+ reason: transition.reason,
173
+ };
174
+ }
175
+
176
+ if (state === 'disabled') {
177
+ entry.disabledAt = new Date().toISOString();
178
+ } else {
179
+ delete entry.disabledAt;
180
+ }
181
+
182
+ if (state === 'error') {
183
+ // errorMessage is set externally if needed; keep existing value
184
+ } else {
185
+ delete entry.errorMessage;
186
+ }
187
+
188
+ this.dirty = true;
189
+ this.save();
190
+ }
191
+
144
192
  private load(): LockfileData {
145
193
  if (!existsSync(this.filePath)) {
146
- return { version: 1, packs: {} };
194
+ return { version: LOCKFILE_VERSION, packs: {} };
147
195
  }
148
196
  try {
149
197
  const raw = JSON.parse(readFileSync(this.filePath, 'utf-8'));
150
- if (raw.version === 1 && typeof raw.packs === 'object') {
198
+ if (typeof raw.packs !== 'object') {
199
+ return { version: LOCKFILE_VERSION, packs: {} };
200
+ }
201
+
202
+ // Migrate v1 → v2: add lifecycle fields
203
+ if (raw.version === 1) {
204
+ for (const entry of Object.values(raw.packs) as LockEntry[]) {
205
+ if (!entry.state) {
206
+ entry.state = 'ready';
207
+ }
208
+ }
209
+ raw.version = LOCKFILE_VERSION;
210
+ // Mark dirty so the migrated data gets persisted on next save
211
+ this.dirty = true;
212
+ }
213
+
214
+ if (raw.version === LOCKFILE_VERSION) {
151
215
  return raw as LockfileData;
152
216
  }
153
- return { version: 1, packs: {} };
217
+
218
+ return { version: LOCKFILE_VERSION, packs: {} };
154
219
  } catch {
155
- return { version: 1, packs: {} };
220
+ return { version: LOCKFILE_VERSION, packs: {} };
156
221
  }
157
222
  }
158
223
  }