codekin 0.4.0 → 0.5.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 (83) hide show
  1. package/README.md +12 -15
  2. package/bin/codekin.mjs +52 -32
  3. package/dist/assets/index-BwKZeT4V.css +1 -0
  4. package/dist/assets/index-CfBnNU24.js +186 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +2 -7
  7. package/server/dist/approval-manager.d.ts +7 -2
  8. package/server/dist/approval-manager.js +45 -79
  9. package/server/dist/approval-manager.js.map +1 -1
  10. package/server/dist/claude-process.d.ts +23 -3
  11. package/server/dist/claude-process.js +123 -23
  12. package/server/dist/claude-process.js.map +1 -1
  13. package/server/dist/commit-event-handler.js.map +1 -1
  14. package/server/dist/config.d.ts +4 -0
  15. package/server/dist/config.js +17 -0
  16. package/server/dist/config.js.map +1 -1
  17. package/server/dist/diff-manager.d.ts +41 -0
  18. package/server/dist/diff-manager.js +303 -0
  19. package/server/dist/diff-manager.js.map +1 -0
  20. package/server/dist/diff-parser.js +8 -2
  21. package/server/dist/diff-parser.js.map +1 -1
  22. package/server/dist/error-page.d.ts +5 -0
  23. package/server/dist/error-page.js +144 -0
  24. package/server/dist/error-page.js.map +1 -0
  25. package/server/dist/native-permissions.d.ts +44 -0
  26. package/server/dist/native-permissions.js +163 -0
  27. package/server/dist/native-permissions.js.map +1 -0
  28. package/server/dist/orchestrator-children.d.ts +74 -0
  29. package/server/dist/orchestrator-children.js +281 -0
  30. package/server/dist/orchestrator-children.js.map +1 -0
  31. package/server/dist/orchestrator-learning.d.ts +134 -0
  32. package/server/dist/orchestrator-learning.js +567 -0
  33. package/server/dist/orchestrator-learning.js.map +1 -0
  34. package/server/dist/orchestrator-manager.d.ts +25 -0
  35. package/server/dist/orchestrator-manager.js +353 -0
  36. package/server/dist/orchestrator-manager.js.map +1 -0
  37. package/server/dist/orchestrator-memory.d.ts +77 -0
  38. package/server/dist/orchestrator-memory.js +288 -0
  39. package/server/dist/orchestrator-memory.js.map +1 -0
  40. package/server/dist/orchestrator-monitor.d.ts +59 -0
  41. package/server/dist/orchestrator-monitor.js +238 -0
  42. package/server/dist/orchestrator-monitor.js.map +1 -0
  43. package/server/dist/orchestrator-reports.d.ts +45 -0
  44. package/server/dist/orchestrator-reports.js +124 -0
  45. package/server/dist/orchestrator-reports.js.map +1 -0
  46. package/server/dist/orchestrator-routes.d.ts +17 -0
  47. package/server/dist/orchestrator-routes.js +526 -0
  48. package/server/dist/orchestrator-routes.js.map +1 -0
  49. package/server/dist/session-archive.js +9 -2
  50. package/server/dist/session-archive.js.map +1 -1
  51. package/server/dist/session-manager.d.ts +101 -39
  52. package/server/dist/session-manager.js +573 -397
  53. package/server/dist/session-manager.js.map +1 -1
  54. package/server/dist/session-naming.d.ts +6 -10
  55. package/server/dist/session-naming.js +60 -62
  56. package/server/dist/session-naming.js.map +1 -1
  57. package/server/dist/session-persistence.d.ts +6 -1
  58. package/server/dist/session-persistence.js +6 -0
  59. package/server/dist/session-persistence.js.map +1 -1
  60. package/server/dist/session-restart-scheduler.d.ts +30 -0
  61. package/server/dist/session-restart-scheduler.js +41 -0
  62. package/server/dist/session-restart-scheduler.js.map +1 -0
  63. package/server/dist/session-routes.js +127 -58
  64. package/server/dist/session-routes.js.map +1 -1
  65. package/server/dist/stepflow-types.d.ts +1 -1
  66. package/server/dist/tsconfig.tsbuildinfo +1 -1
  67. package/server/dist/types.d.ts +34 -2
  68. package/server/dist/types.js +8 -1
  69. package/server/dist/types.js.map +1 -1
  70. package/server/dist/upload-routes.js +13 -4
  71. package/server/dist/upload-routes.js.map +1 -1
  72. package/server/dist/version-check.d.ts +17 -0
  73. package/server/dist/version-check.js +89 -0
  74. package/server/dist/version-check.js.map +1 -0
  75. package/server/dist/workflow-engine.d.ts +74 -1
  76. package/server/dist/workflow-engine.js +20 -1
  77. package/server/dist/workflow-engine.js.map +1 -1
  78. package/server/dist/ws-message-handler.js +115 -9
  79. package/server/dist/ws-message-handler.js.map +1 -1
  80. package/server/dist/ws-server.js +90 -15
  81. package/server/dist/ws-server.js.map +1 -1
  82. package/dist/assets/index-BAdQqYEY.js +0 -182
  83. package/dist/assets/index-CeZYNLWt.css +0 -1
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Orchestrator self-improving memory — extraction, deduplication, aging,
3
+ * pattern learning, and user skill modeling.
4
+ *
5
+ * This module adds the intelligence layer on top of OrchestratorMemory:
6
+ * - Extracts memory candidates from session interactions
7
+ * - Deduplicates against existing memories using FTS similarity
8
+ * - Ages and decays stale items on a schedule
9
+ * - Tracks finding outcomes to improve future triage
10
+ * - Maintains a user skill model that adapts guidance
11
+ * - Records decisions with outcome tracking
12
+ */
13
+ import { OrchestratorMemory, type MemoryType, type MemoryItem } from './orchestrator-memory.js';
14
+ /** A candidate extracted from a session interaction. */
15
+ export interface MemoryCandidate {
16
+ memoryType: MemoryType;
17
+ title: string;
18
+ content: string;
19
+ scope: string | null;
20
+ tags: string[];
21
+ confidence: number;
22
+ }
23
+ /** Finding outcome — tracks what happened when a finding was acted on or skipped. */
24
+ export interface FindingOutcome {
25
+ findingId: string;
26
+ repo: string;
27
+ category: string;
28
+ severity: string;
29
+ action: 'implemented' | 'skipped' | 'deferred';
30
+ reason: string;
31
+ sessionId: string | null;
32
+ outcome: 'success' | 'failure' | 'pending' | null;
33
+ timestamp: string;
34
+ }
35
+ /** User skill assessment per domain. */
36
+ export interface SkillLevel {
37
+ domain: string;
38
+ level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
39
+ confidence: number;
40
+ signals: string[];
41
+ lastUpdated: string;
42
+ }
43
+ /** A decision record with outcome tracking. */
44
+ export interface DecisionRecord {
45
+ id: string;
46
+ decision: string;
47
+ rationale: string;
48
+ repo: string | null;
49
+ relatedFinding: string | null;
50
+ expectedOutcome: string;
51
+ actualOutcome: string | null;
52
+ outcomeAssessedAt: string | null;
53
+ timestamp: string;
54
+ }
55
+ /**
56
+ * Extract memory candidates from an interaction transcript.
57
+ *
58
+ * This is a rule-based extractor that looks for patterns indicating
59
+ * durable information worth remembering. In future, this could be
60
+ * enhanced with an LLM call for richer extraction.
61
+ */
62
+ export declare function extractMemoryCandidates(userMessage: string, assistantResponse: string, currentRepo: string | null): MemoryCandidate[];
63
+ /**
64
+ * Check if a candidate is a duplicate of an existing memory item.
65
+ * Uses FTS search to find similar items, then compares content overlap.
66
+ */
67
+ export declare function findDuplicate(memory: OrchestratorMemory, candidate: MemoryCandidate, threshold?: number): MemoryItem | null;
68
+ /**
69
+ * Smart upsert: insert candidate if no duplicate exists, update if similar.
70
+ * Returns the memory item ID and whether it was new or updated.
71
+ */
72
+ export declare function smartUpsert(memory: OrchestratorMemory, candidate: MemoryCandidate, sourceRef?: string | null): {
73
+ id: string;
74
+ action: 'inserted' | 'updated' | 'skipped';
75
+ };
76
+ /**
77
+ * Run the aging/decay cycle:
78
+ * 1. Expire items past their TTL
79
+ * 2. Compact old journal entries into monthly summaries
80
+ * 3. Decay confidence of items that haven't been accessed recently
81
+ */
82
+ export declare function runAgingCycle(memory: OrchestratorMemory): {
83
+ expired: number;
84
+ compacted: number;
85
+ decayed: number;
86
+ };
87
+ /**
88
+ * Record the outcome of a finding triage decision.
89
+ */
90
+ export declare function recordFindingOutcome(memory: OrchestratorMemory, outcome: FindingOutcome): string;
91
+ /**
92
+ * Analyze past finding outcomes to compute a triage recommendation.
93
+ * Returns the likely action and confidence based on historical patterns.
94
+ */
95
+ export declare function getTriageRecommendation(memory: OrchestratorMemory, category: string, severity: string, repo: string | null): {
96
+ action: 'implement' | 'skip' | 'defer' | 'unknown';
97
+ confidence: number;
98
+ reason: string;
99
+ };
100
+ /** Load the user's skill profile from disk. */
101
+ export declare function loadSkillProfile(): SkillLevel[];
102
+ /** Save the skill profile to disk. */
103
+ export declare function saveSkillProfile(profile: SkillLevel[]): void;
104
+ /**
105
+ * Update the user's skill level for a domain based on observed signals.
106
+ * Signals are things like: "used advanced git rebase", "asked basic TypeScript question",
107
+ * "configured CI pipeline without help".
108
+ */
109
+ export declare function updateSkillLevel(domain: string, signal: string, indicatedLevel: 'beginner' | 'intermediate' | 'advanced' | 'expert'): SkillLevel;
110
+ /**
111
+ * Get the user's assessed skill level for a domain.
112
+ * Returns null if we have no data for this domain.
113
+ */
114
+ export declare function getSkillLevel(domain: string): SkillLevel | null;
115
+ /**
116
+ * Get a guidance style recommendation based on the user's overall skill profile.
117
+ */
118
+ export declare function getGuidanceStyle(): {
119
+ tone: 'tutorial' | 'collaborative' | 'concise';
120
+ explainLevel: 'detailed' | 'moderate' | 'minimal';
121
+ domains: Record<string, SkillLevel['level']>;
122
+ };
123
+ /**
124
+ * Record a decision with expected outcome.
125
+ */
126
+ export declare function recordDecision(memory: OrchestratorMemory, decision: Omit<DecisionRecord, 'id' | 'timestamp' | 'actualOutcome' | 'outcomeAssessedAt'>): string;
127
+ /**
128
+ * Update a decision's actual outcome.
129
+ */
130
+ export declare function assessDecisionOutcome(memory: OrchestratorMemory, decisionId: string, actualOutcome: string): boolean;
131
+ /**
132
+ * Get decisions that need outcome assessment (older than 7 days, no outcome yet).
133
+ */
134
+ export declare function getPendingOutcomeAssessments(memory: OrchestratorMemory): DecisionRecord[];
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Orchestrator self-improving memory — extraction, deduplication, aging,
3
+ * pattern learning, and user skill modeling.
4
+ *
5
+ * This module adds the intelligence layer on top of OrchestratorMemory:
6
+ * - Extracts memory candidates from session interactions
7
+ * - Deduplicates against existing memories using FTS similarity
8
+ * - Ages and decays stale items on a schedule
9
+ * - Tracks finding outcomes to improve future triage
10
+ * - Maintains a user skill model that adapts guidance
11
+ * - Records decisions with outcome tracking
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { ORCHESTRATOR_DIR } from './orchestrator-manager.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Default TTLs (in days)
18
+ // ---------------------------------------------------------------------------
19
+ const TTL_DAYS = {
20
+ user_preference: null, // permanent (pinned)
21
+ repo_context: null, // permanent until repo removed
22
+ decision: 90,
23
+ finding_outcome: 180,
24
+ session_summary: 60,
25
+ journal: 30,
26
+ };
27
+ // ---------------------------------------------------------------------------
28
+ // Memory Extraction
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Extract memory candidates from an interaction transcript.
32
+ *
33
+ * This is a rule-based extractor that looks for patterns indicating
34
+ * durable information worth remembering. In future, this could be
35
+ * enhanced with an LLM call for richer extraction.
36
+ */
37
+ export function extractMemoryCandidates(userMessage, assistantResponse, currentRepo) {
38
+ const candidates = [];
39
+ // Pattern 1: User states a preference ("I prefer...", "always use...", "don't...")
40
+ const prefPatterns = [
41
+ /\bi\s+prefer\b/i,
42
+ /\balways\s+(use|do|run|make)\b/i,
43
+ /\bdon'?t\s+(ever|always)\b/i,
44
+ /\bnever\s+(do|use|run|make)\b/i,
45
+ /\bplease\s+always\b/i,
46
+ /\bmy\s+preference\s+is\b/i,
47
+ ];
48
+ for (const pat of prefPatterns) {
49
+ if (pat.test(userMessage)) {
50
+ candidates.push({
51
+ memoryType: 'user_preference',
52
+ title: 'User preference',
53
+ content: userMessage.slice(0, 500),
54
+ scope: null,
55
+ tags: ['preference', 'user-stated'],
56
+ confidence: 0.9,
57
+ });
58
+ break;
59
+ }
60
+ }
61
+ // Pattern 2: User describes their role or expertise
62
+ const rolePatterns = [
63
+ /\bi(?:'m| am)\s+a\s+(senior|junior|lead|staff|principal|intern)?\s*\w*/i,
64
+ /\bmy\s+role\s+is\b/i,
65
+ /\bi\s+work\s+(?:as|on|in|with)\b/i,
66
+ /\bi(?:'ve| have)\s+been\s+(?:doing|working|coding|programming)\b/i,
67
+ /\bfirst\s+time\s+(?:using|working|touching|doing)\b/i,
68
+ /\bnew\s+to\s+(?:this|react|go|python|typescript|rust|devops)\b/i,
69
+ ];
70
+ for (const pat of rolePatterns) {
71
+ if (pat.test(userMessage)) {
72
+ candidates.push({
73
+ memoryType: 'user_preference',
74
+ title: 'User background',
75
+ content: userMessage.slice(0, 500),
76
+ scope: null,
77
+ tags: ['background', 'skill-signal'],
78
+ confidence: 0.7,
79
+ });
80
+ break;
81
+ }
82
+ }
83
+ // Pattern 3: Explicit "remember" requests
84
+ if (/\bremember\s+(?:that|this|to)\b/i.test(userMessage)) {
85
+ candidates.push({
86
+ memoryType: 'user_preference',
87
+ title: 'User asked to remember',
88
+ content: userMessage.slice(0, 500),
89
+ scope: currentRepo,
90
+ tags: ['explicit-remember'],
91
+ confidence: 1.0,
92
+ });
93
+ }
94
+ // Pattern 4: Decision made ("let's go with...", "decided to...")
95
+ const decisionPatterns = [
96
+ /\blet'?s?\s+(?:go\s+with|use|do|pick|choose)\b/i,
97
+ /\bdecided?\s+(?:to|on|that)\b/i,
98
+ /\bwe(?:'ll| will)\s+(?:go|use|do)\b/i,
99
+ /\bsounds?\s+good\b/i,
100
+ /\byes,?\s+(?:go\s+ahead|do\s+it|proceed|let's)\b/i,
101
+ ];
102
+ for (const pat of decisionPatterns) {
103
+ if (pat.test(userMessage)) {
104
+ candidates.push({
105
+ memoryType: 'decision',
106
+ title: 'Decision',
107
+ content: `User: ${userMessage.slice(0, 250)}\nContext: ${assistantResponse.slice(0, 250)}`,
108
+ scope: currentRepo,
109
+ tags: ['decision'],
110
+ confidence: 0.7,
111
+ });
112
+ break;
113
+ }
114
+ }
115
+ // Pattern 5: Repo-specific context from assistant responses
116
+ if (currentRepo && assistantResponse.length > 200) {
117
+ const repoSignals = [
118
+ /\bthis\s+(?:repo|repository|project|codebase)\s+(?:is|uses|has|was)\b/i,
119
+ /\bthe\s+(?:main|primary)\s+(?:stack|framework|language)\b/i,
120
+ /\barchitecture\s+(?:is|uses|follows)\b/i,
121
+ ];
122
+ for (const pat of repoSignals) {
123
+ if (pat.test(assistantResponse)) {
124
+ candidates.push({
125
+ memoryType: 'repo_context',
126
+ title: `Repo context: ${currentRepo.split('/').pop()}`,
127
+ content: assistantResponse.slice(0, 500),
128
+ scope: currentRepo,
129
+ tags: ['repo-context', 'auto-extracted'],
130
+ confidence: 0.6,
131
+ });
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ return candidates;
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Deduplication
140
+ // ---------------------------------------------------------------------------
141
+ /**
142
+ * Check if a candidate is a duplicate of an existing memory item.
143
+ * Uses FTS search to find similar items, then compares content overlap.
144
+ */
145
+ export function findDuplicate(memory, candidate, threshold = 0.85) {
146
+ // Search for similar items by title/content keywords
147
+ const searchTerms = (candidate.title + ' ' + candidate.content)
148
+ .replace(/[^\w\s]/g, ' ')
149
+ .split(/\s+/)
150
+ .filter(w => w.length > 3)
151
+ .slice(0, 5)
152
+ .join(' ');
153
+ if (!searchTerms)
154
+ return null;
155
+ try {
156
+ const similar = memory.search(searchTerms, 5);
157
+ for (const item of similar) {
158
+ // Same type and scope
159
+ if (item.memoryType !== candidate.memoryType)
160
+ continue;
161
+ if (item.scope !== candidate.scope)
162
+ continue;
163
+ // Simple content overlap check
164
+ const similarity = computeOverlap(item.content, candidate.content);
165
+ if (similarity >= threshold)
166
+ return item;
167
+ }
168
+ }
169
+ catch {
170
+ // FTS query may fail on unusual input — not a problem
171
+ }
172
+ return null;
173
+ }
174
+ /** Compute word-level Jaccard similarity between two texts. */
175
+ function computeOverlap(a, b) {
176
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
177
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
178
+ if (wordsA.size === 0 || wordsB.size === 0)
179
+ return 0;
180
+ let intersection = 0;
181
+ for (const w of wordsA) {
182
+ if (wordsB.has(w))
183
+ intersection++;
184
+ }
185
+ return intersection / (wordsA.size + wordsB.size - intersection);
186
+ }
187
+ /**
188
+ * Smart upsert: insert candidate if no duplicate exists, update if similar.
189
+ * Returns the memory item ID and whether it was new or updated.
190
+ */
191
+ export function smartUpsert(memory, candidate, sourceRef = null) {
192
+ const duplicate = findDuplicate(memory, candidate);
193
+ if (duplicate) {
194
+ // If the new candidate has higher confidence, update
195
+ if (candidate.confidence > duplicate.confidence) {
196
+ const id = memory.upsert({
197
+ id: duplicate.id,
198
+ ...candidate,
199
+ sourceRef,
200
+ isPinned: duplicate.isPinned,
201
+ expiresAt: duplicate.expiresAt,
202
+ });
203
+ return { id, action: 'updated' };
204
+ }
205
+ return { id: duplicate.id, action: 'skipped' };
206
+ }
207
+ // Compute expiry based on type
208
+ const ttlDays = TTL_DAYS[candidate.memoryType];
209
+ const expiresAt = ttlDays
210
+ ? new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000).toISOString()
211
+ : null;
212
+ const id = memory.upsert({
213
+ ...candidate,
214
+ sourceRef,
215
+ isPinned: candidate.memoryType === 'user_preference',
216
+ expiresAt,
217
+ });
218
+ return { id, action: 'inserted' };
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // Aging & Decay
222
+ // ---------------------------------------------------------------------------
223
+ /**
224
+ * Run the aging/decay cycle:
225
+ * 1. Expire items past their TTL
226
+ * 2. Compact old journal entries into monthly summaries
227
+ * 3. Decay confidence of items that haven't been accessed recently
228
+ */
229
+ export function runAgingCycle(memory) {
230
+ // 1. Expire stale items
231
+ const expired = memory.expireStale();
232
+ // 2. Compact old journal entries (older than 30 days) into monthly summaries
233
+ const compacted = compactOldJournals();
234
+ // 3. Decay confidence of old, non-pinned items that haven't been updated
235
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
236
+ const oldItems = memory.list({ limit: 100 })
237
+ .filter(item => !item.isPinned &&
238
+ item.updatedAt < thirtyDaysAgo &&
239
+ item.confidence > 0.3);
240
+ let decayed = 0;
241
+ for (const item of oldItems) {
242
+ const newConfidence = Math.max(0.3, item.confidence * 0.95); // 5% decay
243
+ if (newConfidence < item.confidence) {
244
+ memory.upsert({
245
+ id: item.id,
246
+ memoryType: item.memoryType,
247
+ scope: item.scope,
248
+ title: item.title,
249
+ content: item.content,
250
+ sourceRef: item.sourceRef,
251
+ confidence: newConfidence,
252
+ expiresAt: item.expiresAt,
253
+ isPinned: item.isPinned,
254
+ tags: item.tags,
255
+ });
256
+ decayed++;
257
+ }
258
+ }
259
+ return { expired, compacted, decayed };
260
+ }
261
+ /** Compact journal entries older than 30 days into monthly summary files. */
262
+ function compactOldJournals() {
263
+ const journalDir = join(ORCHESTRATOR_DIR, 'journal');
264
+ if (!existsSync(journalDir))
265
+ return 0;
266
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
267
+ const cutoffDate = thirtyDaysAgo.toISOString().slice(0, 10);
268
+ let files;
269
+ try {
270
+ files = readdirSync(journalDir).filter(f => f.endsWith('.md'));
271
+ }
272
+ catch {
273
+ return 0;
274
+ }
275
+ // Group old files by month
276
+ const monthGroups = new Map();
277
+ for (const file of files) {
278
+ const dateMatch = file.match(/^(\d{4}-\d{2})-\d{2}\.md$/);
279
+ if (!dateMatch)
280
+ continue;
281
+ const fileDate = file.replace('.md', '');
282
+ if (fileDate >= cutoffDate)
283
+ continue; // not old enough
284
+ const month = dateMatch[1];
285
+ const group = monthGroups.get(month) ?? [];
286
+ group.push(file);
287
+ monthGroups.set(month, group);
288
+ }
289
+ // Compact each month into a summary file
290
+ let compacted = 0;
291
+ for (const [month, monthFiles] of monthGroups) {
292
+ const summaryFile = join(journalDir, `${month}-summary.md`);
293
+ if (existsSync(summaryFile))
294
+ continue; // already compacted
295
+ const contents = [`# Journal Summary: ${month}\n`];
296
+ for (const file of monthFiles.sort()) {
297
+ const filePath = join(journalDir, file);
298
+ try {
299
+ const content = readFileSync(filePath, 'utf-8');
300
+ contents.push(`## ${file.replace('.md', '')}\n${content}\n`);
301
+ }
302
+ catch {
303
+ continue;
304
+ }
305
+ }
306
+ writeFileSync(summaryFile, contents.join('\n'), 'utf-8');
307
+ compacted += monthFiles.length;
308
+ // Note: we don't delete originals — the orchestrator can do that if it wants
309
+ }
310
+ return compacted;
311
+ }
312
+ // ---------------------------------------------------------------------------
313
+ // Pattern Learning — Finding Outcomes
314
+ // ---------------------------------------------------------------------------
315
+ /**
316
+ * Record the outcome of a finding triage decision.
317
+ */
318
+ export function recordFindingOutcome(memory, outcome) {
319
+ return memory.upsert({
320
+ memoryType: 'finding_outcome',
321
+ title: `${outcome.action}: ${outcome.category} finding in ${outcome.repo.split('/').pop()}`,
322
+ content: JSON.stringify(outcome),
323
+ scope: outcome.repo,
324
+ sourceRef: outcome.sessionId,
325
+ confidence: 0.9,
326
+ isPinned: false,
327
+ expiresAt: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString(),
328
+ tags: [outcome.category, outcome.severity, outcome.action],
329
+ });
330
+ }
331
+ /**
332
+ * Analyze past finding outcomes to compute a triage recommendation.
333
+ * Returns the likely action and confidence based on historical patterns.
334
+ */
335
+ export function getTriageRecommendation(memory, category, severity, repo) {
336
+ // Get all finding outcomes for this category, prefer repo-specific matches
337
+ const allOutcomes = memory.list({ memoryType: 'finding_outcome', limit: 50 })
338
+ .map(item => {
339
+ try {
340
+ return JSON.parse(item.content);
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ })
346
+ .filter((o) => o !== null)
347
+ .filter(o => o.category === category);
348
+ // Prefer repo-specific outcomes if available
349
+ const repoSpecific = repo ? allOutcomes.filter(o => o.repo === repo) : [];
350
+ const outcomes = repoSpecific.length >= 2 ? repoSpecific : allOutcomes;
351
+ if (outcomes.length < 3) {
352
+ return { action: 'unknown', confidence: 0, reason: 'Not enough historical data' };
353
+ }
354
+ // Filter to matching severity
355
+ const bySeverity = outcomes.filter(o => o.severity === severity);
356
+ const pool = bySeverity.length >= 2 ? bySeverity : outcomes;
357
+ // Count actions
358
+ const counts = { implemented: 0, skipped: 0, deferred: 0 };
359
+ for (const o of pool) {
360
+ counts[o.action]++;
361
+ }
362
+ const total = pool.length;
363
+ const implementRate = counts.implemented / total;
364
+ const skipRate = counts.skipped / total;
365
+ // Also factor in success rate of implementations
366
+ const implementations = pool.filter(o => o.action === 'implemented');
367
+ const successRate = implementations.length > 0
368
+ ? implementations.filter(o => o.outcome === 'success').length / implementations.length
369
+ : 0;
370
+ if (implementRate > 0.6 && successRate > 0.5) {
371
+ return {
372
+ action: 'implement',
373
+ confidence: Math.min(0.9, implementRate * successRate),
374
+ reason: `${Math.round(implementRate * 100)}% of similar findings were implemented (${Math.round(successRate * 100)}% success rate)`,
375
+ };
376
+ }
377
+ if (skipRate > 0.6) {
378
+ return {
379
+ action: 'skip',
380
+ confidence: Math.min(0.85, skipRate),
381
+ reason: `${Math.round(skipRate * 100)}% of similar findings were skipped`,
382
+ };
383
+ }
384
+ return {
385
+ action: 'unknown',
386
+ confidence: 0.3,
387
+ reason: `Mixed history: ${counts.implemented} implemented, ${counts.skipped} skipped, ${counts.deferred} deferred`,
388
+ };
389
+ }
390
+ // ---------------------------------------------------------------------------
391
+ // User Skill Model
392
+ // ---------------------------------------------------------------------------
393
+ const SKILL_PROFILE_FILE = join(ORCHESTRATOR_DIR, 'skill-profile.json');
394
+ /** Load the user's skill profile from disk. */
395
+ export function loadSkillProfile() {
396
+ if (!existsSync(SKILL_PROFILE_FILE))
397
+ return [];
398
+ try {
399
+ return JSON.parse(readFileSync(SKILL_PROFILE_FILE, 'utf-8'));
400
+ }
401
+ catch {
402
+ return [];
403
+ }
404
+ }
405
+ /** Save the skill profile to disk. */
406
+ export function saveSkillProfile(profile) {
407
+ writeFileSync(SKILL_PROFILE_FILE, JSON.stringify(profile, null, 2), 'utf-8');
408
+ }
409
+ /**
410
+ * Update the user's skill level for a domain based on observed signals.
411
+ * Signals are things like: "used advanced git rebase", "asked basic TypeScript question",
412
+ * "configured CI pipeline without help".
413
+ */
414
+ export function updateSkillLevel(domain, signal, indicatedLevel) {
415
+ const profile = loadSkillProfile();
416
+ const existing = profile.find(s => s.domain === domain);
417
+ const now = new Date().toISOString();
418
+ if (existing) {
419
+ // Add signal
420
+ existing.signals.push(signal);
421
+ if (existing.signals.length > 20)
422
+ existing.signals = existing.signals.slice(-20);
423
+ existing.lastUpdated = now;
424
+ // Adjust level — only move up, or down if strong evidence
425
+ const levels = ['beginner', 'intermediate', 'advanced', 'expert'];
426
+ const currentIdx = levels.indexOf(existing.level);
427
+ const newIdx = levels.indexOf(indicatedLevel);
428
+ if (newIdx > currentIdx) {
429
+ // Level up — require confidence
430
+ existing.confidence = Math.min(1, existing.confidence + 0.15);
431
+ if (existing.confidence >= 0.7) {
432
+ existing.level = indicatedLevel;
433
+ existing.confidence = 0.6; // reset after level change
434
+ }
435
+ }
436
+ else if (newIdx < currentIdx) {
437
+ // Level down — be cautious
438
+ existing.confidence = Math.max(0, existing.confidence - 0.1);
439
+ if (existing.confidence < 0.3) {
440
+ existing.level = indicatedLevel;
441
+ existing.confidence = 0.5;
442
+ }
443
+ }
444
+ else {
445
+ // Same level — increase confidence
446
+ existing.confidence = Math.min(1, existing.confidence + 0.1);
447
+ }
448
+ saveSkillProfile(profile);
449
+ return existing;
450
+ }
451
+ // New domain
452
+ const newSkill = {
453
+ domain,
454
+ level: indicatedLevel,
455
+ confidence: 0.5,
456
+ signals: [signal],
457
+ lastUpdated: now,
458
+ };
459
+ profile.push(newSkill);
460
+ saveSkillProfile(profile);
461
+ return newSkill;
462
+ }
463
+ /**
464
+ * Get the user's assessed skill level for a domain.
465
+ * Returns null if we have no data for this domain.
466
+ */
467
+ export function getSkillLevel(domain) {
468
+ const profile = loadSkillProfile();
469
+ return profile.find(s => s.domain === domain) ?? null;
470
+ }
471
+ /**
472
+ * Get a guidance style recommendation based on the user's overall skill profile.
473
+ */
474
+ export function getGuidanceStyle() {
475
+ const profile = loadSkillProfile();
476
+ if (profile.length === 0) {
477
+ return { tone: 'collaborative', explainLevel: 'moderate', domains: {} };
478
+ }
479
+ // Compute average skill level
480
+ const levels = { beginner: 0, intermediate: 1, advanced: 2, expert: 3 };
481
+ const avg = profile.reduce((sum, s) => sum + levels[s.level], 0) / profile.length;
482
+ const domains = {};
483
+ for (const s of profile)
484
+ domains[s.domain] = s.level;
485
+ if (avg < 0.8) {
486
+ return { tone: 'tutorial', explainLevel: 'detailed', domains };
487
+ }
488
+ if (avg < 2.0) {
489
+ return { tone: 'collaborative', explainLevel: 'moderate', domains };
490
+ }
491
+ return { tone: 'concise', explainLevel: 'minimal', domains };
492
+ }
493
+ // ---------------------------------------------------------------------------
494
+ // Decision History
495
+ // ---------------------------------------------------------------------------
496
+ /**
497
+ * Record a decision with expected outcome.
498
+ */
499
+ export function recordDecision(memory, decision) {
500
+ const record = {
501
+ ...decision,
502
+ id: `decision-${Date.now()}`,
503
+ timestamp: new Date().toISOString(),
504
+ actualOutcome: null,
505
+ outcomeAssessedAt: null,
506
+ };
507
+ return memory.upsert({
508
+ memoryType: 'decision',
509
+ title: `Decision: ${decision.decision.slice(0, 100)}`,
510
+ content: JSON.stringify(record),
511
+ scope: decision.repo,
512
+ sourceRef: decision.relatedFinding,
513
+ confidence: 0.8,
514
+ isPinned: false,
515
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
516
+ tags: ['decision'],
517
+ });
518
+ }
519
+ /**
520
+ * Update a decision's actual outcome.
521
+ */
522
+ export function assessDecisionOutcome(memory, decisionId, actualOutcome) {
523
+ const item = memory.get(decisionId);
524
+ if (!item || item.memoryType !== 'decision')
525
+ return false;
526
+ try {
527
+ const record = JSON.parse(item.content);
528
+ record.actualOutcome = actualOutcome;
529
+ record.outcomeAssessedAt = new Date().toISOString();
530
+ memory.upsert({
531
+ id: item.id,
532
+ memoryType: 'decision',
533
+ title: item.title,
534
+ content: JSON.stringify(record),
535
+ scope: item.scope,
536
+ sourceRef: item.sourceRef,
537
+ confidence: item.confidence,
538
+ isPinned: item.isPinned,
539
+ expiresAt: item.expiresAt,
540
+ tags: item.tags,
541
+ });
542
+ return true;
543
+ }
544
+ catch {
545
+ return false;
546
+ }
547
+ }
548
+ /**
549
+ * Get decisions that need outcome assessment (older than 7 days, no outcome yet).
550
+ */
551
+ export function getPendingOutcomeAssessments(memory) {
552
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
553
+ const decisions = memory.list({ memoryType: 'decision', limit: 50 });
554
+ return decisions
555
+ .map(item => {
556
+ try {
557
+ return JSON.parse(item.content);
558
+ }
559
+ catch {
560
+ return null;
561
+ }
562
+ })
563
+ .filter((d) => d !== null &&
564
+ d.actualOutcome === null &&
565
+ d.timestamp < sevenDaysAgo);
566
+ }
567
+ //# sourceMappingURL=orchestrator-learning.js.map