recallnest 1.4.1 → 2.2.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 (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
@@ -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>;