recallnest 1.4.1 → 2.2.1

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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -0,0 +1,208 @@
1
+ /**
2
+ * D-1: Skill Memory — store and retrieve executable skills.
3
+ *
4
+ * Skills are stored as "patterns" category entries with structured metadata,
5
+ * using the same LanceDB vector search infrastructure as regular memories.
6
+ */
7
+
8
+ import type { Embedder } from "./embedder.js";
9
+ import type { MemoryStore, MemoryEntry } from "./store.js";
10
+ import { deterministicId } from "./store.js";
11
+ import { SkillInputSchema, type SkillInput, type StoredSkillRecord } from "./skill-schema.js";
12
+ import { defaultEvolution } from "./memory-evolution.js";
13
+ import { generateAnchor } from "./anchor-generator.js";
14
+ import { buildStructuredMemoryBoundary } from "./memory-boundaries.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ type StoreDeps = Pick<MemoryStore, "store" | "update" | "getById">;
21
+ type EmbedDeps = Pick<Embedder, "embedPassage">;
22
+ type SearchDeps = Pick<MemoryStore, "vectorSearch">;
23
+
24
+ const SKILL_IMPORTANCE = 0.85;
25
+ const SKILL_CATEGORY = "patterns" as const;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function buildEmbeddingText(input: SkillInput): string {
32
+ return `${input.description} ${input.triggerPattern}`;
33
+ }
34
+
35
+ function buildFullText(input: SkillInput): string {
36
+ return [
37
+ `Skill: ${input.name}`,
38
+ `Description: ${input.description}`,
39
+ `Trigger: ${input.triggerPattern}`,
40
+ `Type: ${input.implementationType}`,
41
+ `Implementation: ${input.implementation}`,
42
+ ].join("\n");
43
+ }
44
+
45
+ function buildSkillMetadata(input: SkillInput, anchor: string | undefined): string {
46
+ return JSON.stringify({
47
+ source: input.source,
48
+ tags: input.tags,
49
+ capture: "skill_schema_v1",
50
+ boundary: buildStructuredMemoryBoundary("patterns"),
51
+ canonicalKey: `patterns:skill:${input.name.toLowerCase().replace(/\s+/g, "-")}`,
52
+ evolution: defaultEvolution(),
53
+ ...(anchor ? { anchor } : {}),
54
+ skill: {
55
+ name: input.name,
56
+ description: input.description,
57
+ triggerPattern: input.triggerPattern,
58
+ implementationType: input.implementationType,
59
+ implementation: input.implementation,
60
+ ...(input.inputSchema ? { inputSchema: input.inputSchema } : {}),
61
+ ...(input.verification ? { verification: input.verification } : {}),
62
+ successCount: 0,
63
+ failureCount: 0,
64
+ },
65
+ });
66
+ }
67
+
68
+ function parseSkillFromEntry(entry: MemoryEntry): StoredSkillRecord | null {
69
+ let meta: Record<string, unknown>;
70
+ try {
71
+ meta = JSON.parse(entry.metadata || "{}");
72
+ } catch {
73
+ return null;
74
+ }
75
+
76
+ const skill = meta.skill;
77
+ if (!skill || typeof skill !== "object") return null;
78
+
79
+ const s = skill as Record<string, unknown>;
80
+ if (typeof s.name !== "string" || typeof s.implementation !== "string") return null;
81
+
82
+ return {
83
+ name: s.name as string,
84
+ description: (s.description as string) || "",
85
+ triggerPattern: (s.triggerPattern as string) || "",
86
+ implementationType: (s.implementationType as string) as StoredSkillRecord["implementationType"],
87
+ implementation: s.implementation as string,
88
+ inputSchema: s.inputSchema as Record<string, unknown> | undefined,
89
+ verification: (s.verification as string) || undefined,
90
+ scope: entry.scope,
91
+ source: (meta.source as StoredSkillRecord["source"]) || "manual",
92
+ tags: Array.isArray(meta.tags) ? (meta.tags as string[]) : [],
93
+ id: entry.id,
94
+ storedAt: new Date(entry.timestamp).toISOString(),
95
+ successCount: typeof s.successCount === "number" ? s.successCount : 0,
96
+ failureCount: typeof s.failureCount === "number" ? s.failureCount : 0,
97
+ lastRefinedAt: typeof s.lastRefinedAt === "string" ? s.lastRefinedAt : undefined,
98
+ };
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Public API
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export async function persistSkill(
106
+ store: StoreDeps,
107
+ embedder: EmbedDeps,
108
+ rawInput: unknown,
109
+ ): Promise<StoredSkillRecord> {
110
+ const input = SkillInputSchema.parse(rawInput);
111
+
112
+ const embeddingText = buildEmbeddingText(input);
113
+ const vector = await embedder.embedPassage(embeddingText);
114
+ const fullText = buildFullText(input);
115
+ const id = deterministicId(input.scope, input.name);
116
+ const anchor = generateAnchor(fullText);
117
+ const metadata = buildSkillMetadata(input, anchor);
118
+
119
+ // Check for existing skill with same scope+name (idempotent upsert)
120
+ const existing = await store.getById(id);
121
+
122
+ if (existing) {
123
+ // Update existing skill — bump version in evolution metadata
124
+ let existingMeta: Record<string, unknown> = {};
125
+ try {
126
+ existingMeta = JSON.parse(existing.metadata || "{}");
127
+ } catch { /* use fresh metadata */ }
128
+
129
+ const existingEvolution = existingMeta.evolution as Record<string, unknown> | undefined;
130
+ const currentVersion = typeof existingEvolution?.version === "number" ? existingEvolution.version : 1;
131
+
132
+ const updatedMetadata = JSON.stringify({
133
+ ...JSON.parse(metadata),
134
+ evolution: {
135
+ ...defaultEvolution(),
136
+ version: currentVersion + 1,
137
+ },
138
+ });
139
+
140
+ const updated = await store.update(id, {
141
+ text: fullText,
142
+ vector,
143
+ metadata: updatedMetadata,
144
+ timestamp: Date.now(),
145
+ });
146
+
147
+ if (!updated) {
148
+ throw new Error(`Failed to update skill "${input.name}" (id: ${id})`);
149
+ }
150
+
151
+ return {
152
+ ...input,
153
+ id: updated.id,
154
+ storedAt: new Date(updated.timestamp).toISOString(),
155
+ successCount: 0,
156
+ failureCount: 0,
157
+ };
158
+ }
159
+
160
+ // Store new skill
161
+ const stored = await store.store({
162
+ id,
163
+ text: fullText,
164
+ vector,
165
+ category: SKILL_CATEGORY,
166
+ scope: input.scope,
167
+ importance: SKILL_IMPORTANCE,
168
+ metadata,
169
+ });
170
+
171
+ return {
172
+ ...input,
173
+ id: stored.id,
174
+ storedAt: new Date(stored.timestamp).toISOString(),
175
+ successCount: 0,
176
+ failureCount: 0,
177
+ };
178
+ }
179
+
180
+ export async function retrieveSkills(
181
+ store: SearchDeps,
182
+ embedder: EmbedDeps,
183
+ query: string,
184
+ scope?: string,
185
+ limit?: number,
186
+ ): Promise<Array<{ skill: StoredSkillRecord; score: number }>> {
187
+ const vector = await embedder.embedPassage(query);
188
+ const safeLimit = Math.min(Math.max(limit ?? 3, 1), 10);
189
+
190
+ // Over-fetch to account for filtering
191
+ const fetchLimit = safeLimit * 3;
192
+ const scopeFilter = scope ? [scope] : undefined;
193
+ const results = await store.vectorSearch(vector, fetchLimit, 0.3, scopeFilter);
194
+
195
+ const matched: Array<{ skill: StoredSkillRecord; score: number }> = [];
196
+
197
+ for (const result of results) {
198
+ if (result.entry.category !== SKILL_CATEGORY) continue;
199
+
200
+ const skill = parseSkillFromEntry(result.entry);
201
+ if (!skill) continue;
202
+
203
+ matched.push({ skill, score: result.score });
204
+ if (matched.length >= safeLimit) break;
205
+ }
206
+
207
+ return matched;
208
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * D-2: Case -> Strategy -> Skill Promotion Pipeline.
3
+ *
4
+ * Automatically detects promotion opportunities:
5
+ * - Same scope has N+ similar cases (store_case called repeatedly) -> suggest workflow_pattern
6
+ * - workflow_pattern with structured steps that correlates with multiple cases -> suggest skill
7
+ *
8
+ * Promotion suggestions are returned to the agent for review — never auto-executed,
9
+ * to avoid low-quality skills entering the store.
10
+ */
11
+
12
+ import type { MemoryEntry, MemoryStore } from "./store.js";
13
+ import { cosineSimilarity } from "./multi-vector.js";
14
+ import { isActiveMemory } from "./memory-evolution.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface PromotionCandidate {
21
+ type: "case_to_pattern" | "pattern_to_skill";
22
+ sourceEntries: Array<{ id: string; text: string; score: number }>;
23
+ suggestedName: string;
24
+ suggestedDescription: string;
25
+ /** For pattern_to_skill: extracted implementation steps */
26
+ suggestedImplementation?: string;
27
+ confidence: number; // 0-1
28
+ }
29
+
30
+ export interface PromotionScanResult {
31
+ candidates: PromotionCandidate[];
32
+ scannedCases: number;
33
+ scannedPatterns: number;
34
+ }
35
+
36
+ export interface PromotionConfig {
37
+ /** Minimum similar cases to suggest pattern promotion (default: 3) */
38
+ minCaseOccurrences: number;
39
+ /** Similarity threshold for case clustering (default: 0.75) */
40
+ caseSimilarityThreshold: number;
41
+ /** Max candidates to return (default: 5) */
42
+ maxCandidates: number;
43
+ }
44
+
45
+ const DEFAULT_CONFIG: PromotionConfig = {
46
+ minCaseOccurrences: 3,
47
+ caseSimilarityThreshold: 0.75,
48
+ maxCandidates: 5,
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ type PromotionStore = Pick<MemoryStore, "list" | "vectorSearch">;
56
+
57
+ function isActive(entry: MemoryEntry): boolean {
58
+ return isActiveMemory(entry.metadata);
59
+ }
60
+
61
+ /** Extract a short name from the first case's text (first line or first ~60 chars). */
62
+ function extractName(text: string): string {
63
+ const firstLine = text.split("\n")[0].trim();
64
+ return firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
65
+ }
66
+
67
+ /** Summarize a cluster into a description from member texts. */
68
+ function summarizeCluster(members: MemoryEntry[]): string {
69
+ const snippets = members.slice(0, 3).map(m => {
70
+ const first = m.text.split("\n")[0].trim();
71
+ return first.length > 80 ? first.slice(0, 77) + "..." : first;
72
+ });
73
+ return `Recurring pattern across ${members.length} cases: ${snippets.join("; ")}`;
74
+ }
75
+
76
+ /** Check if text contains structured steps (numbered list or "Steps:" header). */
77
+ function hasStructuredSteps(text: string): boolean {
78
+ return /(?:^|\n)\s*(?:Steps?:|##?\s*Steps?)/i.test(text)
79
+ || /(?:^|\n)\s*[1-9]\.\s+\S/.test(text);
80
+ }
81
+
82
+ /** Extract the steps section from a pattern's text. */
83
+ function extractSteps(text: string): string {
84
+ // Try to find content after "Steps:" header
85
+ const stepsMatch = text.match(/(?:^|\n)\s*(?:Steps?:|##?\s*Steps?)\s*\n([\s\S]+)/i);
86
+ if (stepsMatch) return stepsMatch[1].trim();
87
+
88
+ // Fall back: extract all numbered list items
89
+ const lines = text.split("\n");
90
+ const numbered = lines.filter(l => /^\s*[1-9]\d*\.\s+\S/.test(l));
91
+ return numbered.length > 0 ? numbered.join("\n") : text;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Greedy Clustering (same approach as consolidation-engine's C-2)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ interface Cluster {
99
+ seed: MemoryEntry;
100
+ members: MemoryEntry[];
101
+ }
102
+
103
+ function greedyCluster(
104
+ entries: MemoryEntry[],
105
+ threshold: number,
106
+ ): Cluster[] {
107
+ const clusters: Cluster[] = [];
108
+ const centroids: number[][] = [];
109
+ const assigned = new Set<string>();
110
+
111
+ for (const entry of entries) {
112
+ if (assigned.has(entry.id) || !entry.vector?.length) continue;
113
+
114
+ let bestIdx = -1;
115
+ let bestSim = -1;
116
+
117
+ for (let ci = 0; ci < centroids.length; ci++) {
118
+ const sim = cosineSimilarity(entry.vector, centroids[ci]);
119
+ if (sim > threshold && sim > bestSim) {
120
+ bestSim = sim;
121
+ bestIdx = ci;
122
+ }
123
+ }
124
+
125
+ if (bestIdx >= 0) {
126
+ clusters[bestIdx].members.push(entry);
127
+ assigned.add(entry.id);
128
+ // Update centroid as running average
129
+ const members = clusters[bestIdx].members;
130
+ const dim = centroids[bestIdx].length;
131
+ const newCentroid = new Array<number>(dim);
132
+ for (let d = 0; d < dim; d++) {
133
+ let sum = 0;
134
+ for (const m of members) sum += m.vector[d];
135
+ newCentroid[d] = sum / members.length;
136
+ }
137
+ centroids[bestIdx] = newCentroid;
138
+ } else {
139
+ clusters.push({ seed: entry, members: [entry] });
140
+ centroids.push([...entry.vector]);
141
+ assigned.add(entry.id);
142
+ }
143
+ }
144
+
145
+ return clusters;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Main scan
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Scan for promotion candidates in a given scope.
154
+ *
155
+ * Algorithm:
156
+ * 1. Load all active entries in scope, split into cases and patterns
157
+ * 2. Cluster cases by embedding similarity (greedy clustering)
158
+ * 3. Clusters with >= minCaseOccurrences members -> case_to_pattern candidate
159
+ * 4. Patterns with structured steps that are similar to >= 2 cases -> pattern_to_skill candidate
160
+ */
161
+ export async function scanForPromotions(
162
+ store: PromotionStore,
163
+ scope: string,
164
+ config?: Partial<PromotionConfig>,
165
+ ): Promise<PromotionScanResult> {
166
+ const cfg: PromotionConfig = { ...DEFAULT_CONFIG, ...config };
167
+
168
+ // 1. Load all entries in scope (high limit to get everything meaningful)
169
+ const entries = await store.list([scope], undefined, 500, 0);
170
+ const active = entries.filter(isActive);
171
+
172
+ const cases = active.filter(e => e.category === "cases");
173
+ const patterns = active.filter(e => e.category === "patterns");
174
+
175
+ const result: PromotionScanResult = {
176
+ candidates: [],
177
+ scannedCases: cases.length,
178
+ scannedPatterns: patterns.length,
179
+ };
180
+
181
+ // 2. Cluster cases by vector similarity
182
+ if (cases.length >= cfg.minCaseOccurrences) {
183
+ const clusters = greedyCluster(cases, cfg.caseSimilarityThreshold);
184
+
185
+ for (const cluster of clusters) {
186
+ if (cluster.members.length < cfg.minCaseOccurrences) continue;
187
+ if (result.candidates.length >= cfg.maxCandidates) break;
188
+
189
+ // Compute average intra-cluster similarity for scoring
190
+ const avgSim = computeAverageIntraClusterSimilarity(cluster.members);
191
+
192
+ result.candidates.push({
193
+ type: "case_to_pattern",
194
+ sourceEntries: cluster.members.map(m => ({
195
+ id: m.id,
196
+ text: m.text,
197
+ score: avgSim,
198
+ })),
199
+ suggestedName: extractName(cluster.seed.text),
200
+ suggestedDescription: summarizeCluster(cluster.members),
201
+ confidence: cluster.members.length / (cluster.members.length + 2), // Bayesian smoothing
202
+ });
203
+ }
204
+ }
205
+
206
+ // 3. Detect pattern_to_skill candidates
207
+ for (const pattern of patterns) {
208
+ if (result.candidates.length >= cfg.maxCandidates) break;
209
+ if (!hasStructuredSteps(pattern.text)) continue;
210
+ if (!pattern.vector?.length) continue;
211
+
212
+ // Find cases similar to this pattern
213
+ const similarCases = cases.filter(c => {
214
+ if (!c.vector?.length) return false;
215
+ const sim = cosineSimilarity(pattern.vector, c.vector);
216
+ return sim >= cfg.caseSimilarityThreshold;
217
+ });
218
+
219
+ if (similarCases.length < 2) continue;
220
+
221
+ result.candidates.push({
222
+ type: "pattern_to_skill",
223
+ sourceEntries: [
224
+ { id: pattern.id, text: pattern.text, score: 1.0 },
225
+ ...similarCases.map(c => ({
226
+ id: c.id,
227
+ text: c.text,
228
+ score: cosineSimilarity(pattern.vector, c.vector),
229
+ })),
230
+ ],
231
+ suggestedName: extractName(pattern.text),
232
+ suggestedDescription: `Skill derived from pattern with ${similarCases.length} supporting cases`,
233
+ suggestedImplementation: extractSteps(pattern.text),
234
+ confidence: similarCases.length / (similarCases.length + 2),
235
+ });
236
+ }
237
+
238
+ // Sort by confidence descending, then truncate
239
+ result.candidates.sort((a, b) => b.confidence - a.confidence);
240
+ result.candidates = result.candidates.slice(0, cfg.maxCandidates);
241
+
242
+ return result;
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Formatting
247
+ // ---------------------------------------------------------------------------
248
+
249
+ /** Format scan results for MCP tool output. */
250
+ export function formatPromotionResult(result: PromotionScanResult): string {
251
+ const lines = [
252
+ `Promotion scan: ${result.scannedCases} cases, ${result.scannedPatterns} patterns scanned.`,
253
+ ];
254
+
255
+ if (result.candidates.length === 0) {
256
+ lines.push("No promotion candidates found.");
257
+ return lines.join("\n");
258
+ }
259
+
260
+ lines.push(`Found ${result.candidates.length} candidate(s):\n`);
261
+
262
+ for (const [i, c] of result.candidates.entries()) {
263
+ lines.push(`### ${i + 1}. [${c.type}] ${c.suggestedName}`);
264
+ lines.push(`Confidence: ${(c.confidence * 100).toFixed(1)}%`);
265
+ lines.push(`Description: ${c.suggestedDescription}`);
266
+ lines.push(`Sources: ${c.sourceEntries.length} entries`);
267
+ if (c.suggestedImplementation) {
268
+ lines.push(`Implementation:\n\`\`\`\n${c.suggestedImplementation}\n\`\`\``);
269
+ }
270
+ lines.push("");
271
+ }
272
+
273
+ return lines.join("\n");
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Internal helpers
278
+ // ---------------------------------------------------------------------------
279
+
280
+ function computeAverageIntraClusterSimilarity(members: MemoryEntry[]): number {
281
+ if (members.length < 2) return 1.0;
282
+ let total = 0;
283
+ let pairs = 0;
284
+ for (let i = 0; i < members.length; i++) {
285
+ for (let j = i + 1; j < members.length; j++) {
286
+ if (members[i].vector?.length && members[j].vector?.length) {
287
+ total += cosineSimilarity(members[i].vector, members[j].vector);
288
+ pairs++;
289
+ }
290
+ }
291
+ }
292
+ return pairs > 0 ? total / pairs : 0;
293
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * D-1: Skill Schema — defines the shape of executable skills stored in memory.
3
+ *
4
+ * Skills are "what an agent can do" (vs. memories which are "what an agent learned").
5
+ * Alita/Alita-G showed MCP itself is a natural skill representation format.
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ export const SkillImplementationTypeSchema = z.enum([
11
+ "bash", "python", "mcp_tool_chain", "instruction_sequence",
12
+ ]);
13
+
14
+ export type SkillImplementationType = z.infer<typeof SkillImplementationTypeSchema>;
15
+
16
+ export const SkillInputSchema = z.object({
17
+ name: z.string().min(1).max(120).describe("Unique skill identifier (e.g. 'deploy_production')"),
18
+ description: z.string().min(1).max(500).describe("Natural language description (used for retrieval)"),
19
+ triggerPattern: z.string().min(1).max(300).describe("When to suggest this skill"),
20
+ implementationType: SkillImplementationTypeSchema,
21
+ implementation: z.string().min(1).max(5000).describe("Executable content"),
22
+ inputSchema: z.record(z.string(), z.unknown()).optional().describe("Parameter definition (JSON Schema)"),
23
+ verification: z.string().max(500).optional().describe("How to verify execution success"),
24
+ scope: z.string().min(1).max(160).describe("Project scope"),
25
+ source: z.enum(["manual", "agent", "api"]).default("manual"),
26
+ tags: z.array(z.string().max(60)).max(6).default([]),
27
+ });
28
+
29
+ export type SkillInput = z.infer<typeof SkillInputSchema>;
30
+
31
+ export const StoredSkillRecordSchema = SkillInputSchema.extend({
32
+ id: z.string(),
33
+ storedAt: z.string().datetime(),
34
+ successCount: z.number().default(0),
35
+ failureCount: z.number().default(0),
36
+ lastRefinedAt: z.string().datetime().optional(),
37
+ });
38
+
39
+ export type StoredSkillRecord = z.infer<typeof StoredSkillRecordSchema>;