chati-dev 4.0.11 → 4.1.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 (107) hide show
  1. package/README.md +5 -0
  2. package/bin/chati.js +16 -0
  3. package/framework/agents/build/dev.md +43 -10
  4. package/framework/agents/discover/brief.md +38 -8
  5. package/framework/agents/discover/brownfield-wu.md +44 -3
  6. package/framework/agents/discover/greenfield-wu.md +14 -0
  7. package/framework/agents/plan/architect.md +6 -0
  8. package/framework/agents/plan/detail.md +25 -0
  9. package/framework/agents/plan/tasks.md +29 -1
  10. package/framework/agents/plan/ux-brand-architect.md +215 -0
  11. package/framework/agents/plan/ux-component-engineer.md +289 -0
  12. package/framework/agents/plan/ux-researcher.md +166 -0
  13. package/framework/agents/plan/ux.md +126 -1008
  14. package/framework/agents/quality/qa-implementation.md +121 -22
  15. package/framework/agents/quality/qa-planning.md +18 -0
  16. package/framework/config.yaml +15 -4
  17. package/framework/constitution.md +8 -2
  18. package/framework/context/root.md +1 -1
  19. package/framework/data/entity-registry.yaml +59 -3
  20. package/framework/hooks/constitution-guard.js +67 -1
  21. package/framework/hooks/license-guard.js +4 -4
  22. package/framework/hooks/model-governance.js +2 -1
  23. package/framework/hooks/prism-engine.js +74 -6
  24. package/framework/hooks/read-protection.js +1 -1
  25. package/framework/hooks/session-digest.js +159 -7
  26. package/framework/hooks/settings.json +16 -4
  27. package/framework/hooks/style-guard.js +134 -0
  28. package/framework/hooks/undercover-guard.js +220 -0
  29. package/framework/intelligence/context-engine.md +21 -0
  30. package/framework/intelligence/hooks-performance.md +54 -0
  31. package/framework/intelligence/memory-layer.md +47 -0
  32. package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
  33. package/framework/orchestrator/chati.md +327 -1067
  34. package/framework/templates/brandbook-html-tmpl.md +107 -0
  35. package/framework/templates/session-memory-tmpl.yaml +68 -0
  36. package/package.json +1 -1
  37. package/src/autonomy/build-state.js +1 -1
  38. package/src/autonomy/worktree-manager.js +13 -13
  39. package/src/config/agent-customizer.js +1 -1
  40. package/src/config/gemini-hooks-generator.js +6 -6
  41. package/src/config/ide-configs.js +1 -1
  42. package/src/context/bracket-tracker.js +25 -4
  43. package/src/context/engine.js +37 -7
  44. package/src/context/formatter.js +45 -1
  45. package/src/context/index.js +3 -3
  46. package/src/dashboard/data-reader.js +7 -7
  47. package/src/decision/engine.js +2 -2
  48. package/src/decision/registry-healer.js +1 -1
  49. package/src/decision/registry-updater.js +2 -2
  50. package/src/extensions/loader.js +1 -1
  51. package/src/gates/g1-planning-complete.js +1 -1
  52. package/src/gates/g2-qa-planning.js +1 -1
  53. package/src/gates/g3-implementation.js +4 -4
  54. package/src/gates/g4-qa-implementation.js +2 -2
  55. package/src/health/auto-fix.js +3 -3
  56. package/src/health/engine.js +2 -2
  57. package/src/installer/brownfield-upgrader.js +3 -3
  58. package/src/installer/manifest.js +1 -1
  59. package/src/installer/transaction.js +1 -1
  60. package/src/installer/validator.js +2 -2
  61. package/src/intelligence/decision-engine.js +1 -1
  62. package/src/intelligence/memory-manager.js +1 -1
  63. package/src/intelligence/registry-manager.js +2 -2
  64. package/src/intelligence/timeline.js +1 -1
  65. package/src/license/client.js +1 -1
  66. package/src/license/commands.js +2 -2
  67. package/src/memory/agent-memory.js +3 -3
  68. package/src/memory/daily-digest.js +170 -0
  69. package/src/memory/dream.js +254 -0
  70. package/src/memory/gotchas.js +2 -2
  71. package/src/memory/index.js +18 -0
  72. package/src/memory/magic-docs.js +98 -0
  73. package/src/memory/memory-extractor.js +163 -0
  74. package/src/memory/session-digest.js +144 -6
  75. package/src/merger/yaml-merger.js +1 -1
  76. package/src/orchestrator/cli.js +980 -0
  77. package/src/orchestrator/handoff-engine.js +25 -4
  78. package/src/orchestrator/index.js +9 -0
  79. package/src/orchestrator/session-manager.js +1 -1
  80. package/src/preview/detector.js +3 -3
  81. package/src/preview/launcher.js +2 -2
  82. package/src/quality/metrics-collector.js +1 -1
  83. package/src/quality/test-runner.js +2 -4
  84. package/src/scanning/density-scanner.js +51 -0
  85. package/src/scanning/env-scanner.js +97 -0
  86. package/src/scanning/index.js +7 -0
  87. package/src/scanning/leakage-scanner.js +54 -0
  88. package/src/scanning/placeholder-scanner.js +40 -0
  89. package/src/scanning/security-scanner.js +94 -0
  90. package/src/security/bash-security.js +335 -0
  91. package/src/security/index.js +9 -0
  92. package/src/telemetry/config.js +3 -3
  93. package/src/telemetry/sender.js +1 -1
  94. package/src/terminal/cli-registry.js +1 -1
  95. package/src/terminal/cost-tracker.js +1 -2
  96. package/src/terminal/prompt-builder.js +11 -27
  97. package/src/terminal/rate-limiter.js +1 -2
  98. package/src/terminal/run-agent.js +3 -3
  99. package/src/terminal/spawner.js +41 -4
  100. package/src/upgrade/checker.js +2 -2
  101. package/src/upgrade/migrator.js +1 -1
  102. package/src/utils/config-parser.js +1 -1
  103. package/src/utils/file-lock.js +3 -3
  104. package/src/utils/provider-limits.js +35 -4
  105. package/src/wizard/i18n.js +2 -2
  106. package/src/wizard/index.js +2 -1
  107. package/src/wizard/questions.js +4 -2
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Dream — Memory Consolidation System.
3
+ *
4
+ * 4-phase consolidation inspired by Claude Code's autoDream:
5
+ * Phase 1 (Orient): Scan existing memories, assess landscape
6
+ * Phase 2 (Gather): Read all agent memories and session digests
7
+ * Phase 3 (Consolidate): Deduplicate, merge cross-agent, increment evidence
8
+ * Phase 4 (Prune): Remove cold/low-confidence entries, archive removed
9
+ *
10
+ * Triggered manually via `/chati dream` or programmatically.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { createHash } from 'crypto';
16
+ import { readAgentMemory, getAgentMemoryStats } from './agent-memory.js';
17
+
18
+ const MAX_ENTRIES = 200;
19
+ const MEMORY_BASE = '.chati/memories';
20
+ const ARCHIVE_DIR = '.chati/memories/shared/archive';
21
+
22
+ /**
23
+ * Run the complete 4-phase dream consolidation.
24
+ * @param {string} projectDir - Project root directory
25
+ * @returns {{ phase1: object, phase2: object, phase3: object, phase4: object, summary: object }}
26
+ */
27
+ export function runDreamConsolidation(projectDir) {
28
+ const report = {
29
+ phase1: null,
30
+ phase2: null,
31
+ phase3: null,
32
+ phase4: null,
33
+ summary: { merged: 0, pruned: 0, archived: 0, remaining: 0, contradictions: 0 },
34
+ };
35
+
36
+ // Phase 1: Orient
37
+ report.phase1 = orient(projectDir);
38
+
39
+ // Phase 2: Gather
40
+ report.phase2 = gather(projectDir, report.phase1.agents);
41
+
42
+ // Phase 3: Consolidate
43
+ report.phase3 = consolidate(report.phase2.allEntries);
44
+
45
+ // Phase 4: Prune
46
+ report.phase4 = prune(projectDir, report.phase3.consolidated);
47
+
48
+ // Summary
49
+ report.summary = {
50
+ merged: report.phase3.mergedCount,
51
+ pruned: report.phase4.prunedCount,
52
+ archived: report.phase4.archivedCount,
53
+ remaining: report.phase4.remaining,
54
+ contradictions: report.phase3.contradictions,
55
+ timestamp: new Date().toISOString(),
56
+ };
57
+
58
+ return report;
59
+ }
60
+
61
+ /**
62
+ * Phase 1: Orient — Scan memory landscape.
63
+ */
64
+ function orient(projectDir) {
65
+ const stats = getAgentMemoryStats(projectDir);
66
+ const memoryBase = join(projectDir, MEMORY_BASE);
67
+
68
+ // Count shared memories
69
+ let sharedCount = 0;
70
+ const sharedDir = join(memoryBase, 'shared');
71
+ if (existsSync(sharedDir)) {
72
+ try {
73
+ const gotchasPath = join(sharedDir, 'gotchas.json');
74
+ if (existsSync(gotchasPath)) {
75
+ const data = JSON.parse(readFileSync(gotchasPath, 'utf-8'));
76
+ sharedCount = Array.isArray(data) ? data.length : (data.gotchas || []).length;
77
+ }
78
+ } catch (err) { process.stderr.write(`[chati] dream orient gotchas: ${err.message}\n`); }
79
+ }
80
+
81
+ // List agents with memories
82
+ const agents = [];
83
+ const agentsDir = join(memoryBase);
84
+ if (existsSync(agentsDir)) {
85
+ try {
86
+ const dirs = readdirSync(agentsDir, { withFileTypes: true })
87
+ .filter(d => d.isDirectory() && d.name !== 'shared')
88
+ .map(d => d.name);
89
+ for (const agentName of dirs) {
90
+ const memPath = join(agentsDir, agentName, 'MEMORY.md');
91
+ if (existsSync(memPath)) {
92
+ agents.push(agentName);
93
+ }
94
+ }
95
+ } catch (err) { process.stderr.write(`[chati] dream orient agents: ${err.message}\n`); }
96
+ }
97
+
98
+ return {
99
+ totalAgents: agents.length,
100
+ agents,
101
+ agentStats: stats,
102
+ sharedGotchas: sharedCount,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Phase 2: Gather — Read all agent memories into unified list.
108
+ */
109
+ function gather(projectDir, agentNames) {
110
+ const allEntries = [];
111
+
112
+ for (const agentName of agentNames) {
113
+ const memResult = readAgentMemory(projectDir, agentName);
114
+ if (memResult.loaded && memResult.entries) {
115
+ for (const entry of memResult.entries) {
116
+ allEntries.push({
117
+ ...entry,
118
+ agent: agentName,
119
+ _hash: hashContent(entry.content || ''),
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ return {
126
+ totalEntries: allEntries.length,
127
+ byAgent: agentNames.reduce((acc, name) => {
128
+ acc[name] = allEntries.filter(e => e.agent === name).length;
129
+ return acc;
130
+ }, {}),
131
+ allEntries,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Phase 3: Consolidate — Deduplicate and merge.
137
+ */
138
+ function consolidate(allEntries) {
139
+ const hashMap = new Map();
140
+ let mergedCount = 0;
141
+ let contradictions = 0;
142
+
143
+ for (const entry of allEntries) {
144
+ const existing = hashMap.get(entry._hash);
145
+ if (existing) {
146
+ // Merge: keep highest confidence, combine tags, increment evidence
147
+ mergedCount++;
148
+ existing.confidence = Math.max(existing.confidence || 0, entry.confidence || 0);
149
+ existing.evidence_count = (existing.evidence_count || 1) + (entry.evidence_count || 1);
150
+
151
+ // Merge tags
152
+ const existingTags = new Set(existing.tags || []);
153
+ for (const tag of (entry.tags || [])) {
154
+ existingTags.add(tag);
155
+ }
156
+ existing.tags = [...existingTags];
157
+
158
+ // Track cross-agent corroboration
159
+ if (entry.agent !== existing.agent) {
160
+ existing.scope = 'shared';
161
+ if (!existing._agents) existing._agents = new Set([existing.agent]);
162
+ existing._agents.add(entry.agent);
163
+ }
164
+ } else {
165
+ hashMap.set(entry._hash, { ...entry });
166
+ }
167
+ }
168
+
169
+ const consolidated = [...hashMap.values()];
170
+
171
+ // Detect contradictions: entries from same agent with same type but different content
172
+ const agentTypeMap = new Map();
173
+ for (const entry of consolidated) {
174
+ const key = `${entry.agent}:${entry.type || entry.category || 'unknown'}`;
175
+ const existing = agentTypeMap.get(key);
176
+ if (existing && existing._hash !== entry._hash) {
177
+ contradictions++;
178
+ }
179
+ agentTypeMap.set(key, entry);
180
+ }
181
+
182
+ const cleaned = consolidated.map(({ _hash: _h, _agents: _a, ...clean }) => clean);
183
+
184
+ return {
185
+ consolidated: cleaned,
186
+ mergedCount,
187
+ contradictions,
188
+ uniqueCount: cleaned.length,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Phase 4: Prune — Remove cold entries if over MAX_ENTRIES, archive removed.
194
+ */
195
+ function prune(projectDir, consolidated) {
196
+ if (consolidated.length <= MAX_ENTRIES) {
197
+ return {
198
+ prunedCount: 0,
199
+ archivedCount: 0,
200
+ remaining: consolidated.length,
201
+ };
202
+ }
203
+
204
+ // Sort: cold first, then lowest confidence, then lowest access_count
205
+ const tierOrder = { cold: 0, warm: 1, hot: 2 };
206
+ const sorted = [...consolidated].sort((a, b) => {
207
+ const tierDiff = (tierOrder[a.tier] || 0) - (tierOrder[b.tier] || 0);
208
+ if (tierDiff !== 0) return tierDiff;
209
+ const confDiff = (a.confidence || 0) - (b.confidence || 0);
210
+ if (confDiff !== 0) return confDiff;
211
+ return (a.access_count || 0) - (b.access_count || 0);
212
+ });
213
+
214
+ const toRemove = sorted.slice(0, sorted.length - MAX_ENTRIES);
215
+ const toKeep = sorted.slice(sorted.length - MAX_ENTRIES);
216
+
217
+ // Archive removed entries
218
+ let archivedCount = 0;
219
+ if (toRemove.length > 0) {
220
+ const archiveDir = join(projectDir, ARCHIVE_DIR);
221
+ mkdirSync(archiveDir, { recursive: true });
222
+
223
+ const dateStr = new Date().toISOString().split('T')[0];
224
+ const archivePath = join(archiveDir, `dream-${dateStr}.json`);
225
+
226
+ try {
227
+ let existing = [];
228
+ if (existsSync(archivePath)) {
229
+ existing = JSON.parse(readFileSync(archivePath, 'utf-8'));
230
+ }
231
+ const merged = [...existing, ...toRemove];
232
+ writeFileSync(archivePath, JSON.stringify(merged, null, 2), 'utf-8');
233
+ archivedCount = toRemove.length;
234
+ } catch (err) { process.stderr.write(`[chati] dream prune archive: ${err.message}\n`); }
235
+ }
236
+
237
+ return {
238
+ prunedCount: toRemove.length,
239
+ archivedCount,
240
+ remaining: toKeep.length,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Hash content for deduplication (same approach as gotchas.js).
246
+ */
247
+ function hashContent(text) {
248
+ const normalized = String(text)
249
+ .toLowerCase()
250
+ .replace(/\s+/g, ' ')
251
+ .replace(/\d+/g, 'N')
252
+ .trim();
253
+ return createHash('sha256').update(normalized).digest('hex').slice(0, 16);
254
+ }
@@ -104,7 +104,7 @@ function loadGotchas(projectDir) {
104
104
  try {
105
105
  const content = readFileSync(gotchasPath, 'utf-8');
106
106
  return JSON.parse(content);
107
- } catch {
107
+ } catch { /* expected: file may not exist */
108
108
  return [];
109
109
  }
110
110
  }
@@ -136,7 +136,7 @@ function loadErrorLog(projectDir) {
136
136
  try {
137
137
  const content = readFileSync(errorLogPath, 'utf-8');
138
138
  return JSON.parse(content);
139
- } catch {
139
+ } catch { /* expected: file may not exist */
140
140
  return [];
141
141
  }
142
142
  }
@@ -33,3 +33,21 @@ export {
33
33
  searchAllMemories,
34
34
  getUnifiedMemoryStats,
35
35
  } from './search.js';
36
+
37
+ export {
38
+ extractMemories,
39
+ getExtractionStats,
40
+ } from './memory-extractor.js';
41
+
42
+ export { runDreamConsolidation } from './dream.js';
43
+
44
+ export { updateClaudeMd } from './magic-docs.js';
45
+
46
+ export { buildStructuredDigest } from './session-digest.js';
47
+
48
+ export {
49
+ buildDailyEntry,
50
+ appendDailyDigest,
51
+ loadTodayDigest,
52
+ loadDigestByDate,
53
+ } from './daily-digest.js';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Magic Docs — Auto-updating CLAUDE.md after agent handoffs.
3
+ *
4
+ * Inspired by Claude Code's Magic Docs system that uses `# MAGIC DOC:` headers
5
+ * to trigger background self-updating documentation.
6
+ *
7
+ * This module updates only the `## Current State` section of CLAUDE.md,
8
+ * preserving all other user-written sections untouched.
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ const SECTION_HEADER = '## Current State';
15
+ const SECTION_REGEX = /^## Current State\n([\s\S]*?)(?=\n## |\n---|\n$|$)/m;
16
+
17
+ /**
18
+ * Update CLAUDE.md with current project state after a handoff.
19
+ *
20
+ * @param {string} projectDir - Project root directory
21
+ * @param {object} context - Current state to write
22
+ * @param {string} [context.currentAgent] - Active agent name
23
+ * @param {string} [context.pipelinePosition] - Pipeline phase/step
24
+ * @param {string} [context.activeTask] - Current task ID + title
25
+ * @param {number} [context.progress] - Pipeline completion percentage
26
+ * @param {Array<{what: string, why: string}>} [context.decisions] - Recent decisions
27
+ * @param {string[]} [context.nextSteps] - Planned next steps
28
+ * @returns {{ updated: boolean, path: string }}
29
+ */
30
+ export function updateClaudeMd(projectDir, context = {}) {
31
+ const claudePath = join(projectDir, 'CLAUDE.md');
32
+ const timestamp = new Date().toISOString().split('T')[0];
33
+
34
+ // Build the new section content
35
+ const lines = [];
36
+ lines.push(SECTION_HEADER);
37
+ if (context.currentAgent) lines.push(`- **Agent**: ${context.currentAgent}`);
38
+ if (context.pipelinePosition) lines.push(`- **Pipeline**: ${context.pipelinePosition}`);
39
+ if (context.activeTask) lines.push(`- **Task**: ${context.activeTask}`);
40
+ if (context.progress !== undefined) lines.push(`- **Progress**: ${context.progress}%`);
41
+ lines.push(`- **Last Updated**: ${timestamp}`);
42
+
43
+ if (context.decisions && context.decisions.length > 0) {
44
+ lines.push('');
45
+ lines.push('### Recent Decisions');
46
+ for (const d of context.decisions.slice(0, 5)) {
47
+ const text = typeof d === 'object' ? `${d.what} — ${d.why}` : String(d);
48
+ lines.push(`- ${text}`);
49
+ }
50
+ }
51
+
52
+ if (context.nextSteps && context.nextSteps.length > 0) {
53
+ lines.push('');
54
+ lines.push('### Next Steps');
55
+ for (const step of context.nextSteps.slice(0, 5)) {
56
+ lines.push(`- ${step}`);
57
+ }
58
+ }
59
+
60
+ const newSection = lines.join('\n');
61
+
62
+ try {
63
+ if (!existsSync(claudePath)) {
64
+ // Create CLAUDE.md with just the Current State section
65
+ writeFileSync(claudePath, newSection + '\n', 'utf-8');
66
+ return { updated: true, path: claudePath };
67
+ }
68
+
69
+ const content = readFileSync(claudePath, 'utf-8');
70
+
71
+ // Handle empty file as creation
72
+ if (!content || content.trim().length === 0) {
73
+ writeFileSync(claudePath, newSection + '\n', 'utf-8');
74
+ return { updated: true, path: claudePath };
75
+ }
76
+
77
+ let updatedContent;
78
+ if (SECTION_REGEX.test(content)) {
79
+ // Replace existing section
80
+ updatedContent = content.replace(SECTION_REGEX, newSection);
81
+ } else {
82
+ // Find insertion point — after first heading or at the top
83
+ const firstHeadingEnd = content.indexOf('\n## ');
84
+ if (firstHeadingEnd !== -1) {
85
+ // Insert before the first ## section
86
+ updatedContent = content.slice(0, firstHeadingEnd) + '\n\n' + newSection + '\n' + content.slice(firstHeadingEnd);
87
+ } else {
88
+ // Append at end
89
+ updatedContent = content.trimEnd() + '\n\n' + newSection + '\n';
90
+ }
91
+ }
92
+
93
+ writeFileSync(claudePath, updatedContent, 'utf-8');
94
+ return { updated: true, path: claudePath };
95
+ } catch { /* expected: operation may fail gracefully */
96
+ return { updated: false, path: claudePath };
97
+ }
98
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Memory Extractor — Per-turn automatic memory capture.
3
+ *
4
+ * Analyzes agent output text for patterns indicating decisions, resolutions,
5
+ * corrections, and validated patterns. Extracts up to MAX_PER_TURN memories
6
+ * following the memory.schema.json structure.
7
+ *
8
+ * Inspired by Claude Code's extractMemories system that runs as a forked
9
+ * agent after each complete turn.
10
+ */
11
+
12
+ const MAX_PER_TURN = 3;
13
+
14
+ /**
15
+ * Extraction patterns mapped to memory types and cognitive sectors.
16
+ * Each pattern has: regex, memory type, cognitive sector, confidence base.
17
+ */
18
+ const EXTRACTION_PATTERNS = [
19
+ // Decisions — episodic sector
20
+ {
21
+ patterns: [
22
+ /(?:decided|chose|selected|picked|went with|opted for)\s+(?:to\s+)?(.{10,120})/gi,
23
+ /(?:decision|choice):\s*(.{10,120})/gi,
24
+ ],
25
+ type: 'decision',
26
+ sector: 'episodic',
27
+ confidence: 0.6,
28
+ },
29
+ // Resolutions — procedural sector
30
+ {
31
+ patterns: [
32
+ /(?:fixed|resolved|solved|patched|workaround)\s+(?:by|with|via)\s+(.{10,120})/gi,
33
+ /(?:solution|fix|resolution):\s*(.{10,120})/gi,
34
+ ],
35
+ type: 'resolution',
36
+ sector: 'procedural',
37
+ confidence: 0.7,
38
+ },
39
+ // User corrections — reflective sector
40
+ {
41
+ patterns: [
42
+ /(?:actually|no,?\s+use|instead,?\s+use|correction:)\s+(.{10,120})/gi,
43
+ /(?:user\s+(?:said|wants|prefers|corrected)):?\s*(.{10,120})/gi,
44
+ ],
45
+ type: 'user_correction',
46
+ sector: 'reflective',
47
+ confidence: 0.8,
48
+ },
49
+ // Validated patterns — semantic sector
50
+ {
51
+ patterns: [
52
+ /(?:confirmed|verified|validated|works|proven)\s+(?:that\s+)?(.{10,120})/gi,
53
+ /(?:pattern|approach)\s+(?:works|validated|confirmed):\s*(.{10,120})/gi,
54
+ ],
55
+ type: 'validated_pattern',
56
+ sector: 'semantic',
57
+ confidence: 0.6,
58
+ },
59
+ // Error patterns — episodic sector
60
+ {
61
+ patterns: [
62
+ /(?:error|bug|issue|problem):\s*(.{10,120})/gi,
63
+ /(?:failed|broke|crashed)\s+(?:because|due to|when)\s+(.{10,120})/gi,
64
+ ],
65
+ type: 'error_pattern',
66
+ sector: 'episodic',
67
+ confidence: 0.5,
68
+ },
69
+ ];
70
+
71
+ /**
72
+ * Extract memories from agent output text.
73
+ *
74
+ * @param {string} text - Agent response text to analyze
75
+ * @param {object} context - Current execution context
76
+ * @param {string} context.agent - Active agent name
77
+ * @param {string} [context.task] - Active task ID
78
+ * @param {string} [context.mode] - Current mode
79
+ * @returns {object[]} Array of memory candidates (max MAX_PER_TURN)
80
+ */
81
+ export function extractMemories(text, context = {}) {
82
+ if (!text || typeof text !== 'string' || text.length < 20) return [];
83
+
84
+ const candidates = [];
85
+ const seen = new Set();
86
+
87
+ for (const group of EXTRACTION_PATTERNS) {
88
+ for (const pattern of group.patterns) {
89
+ // Create fresh regex to avoid shared lastIndex state across calls
90
+ const re = new RegExp(pattern.source, pattern.flags);
91
+ let match;
92
+ while ((match = re.exec(text)) !== null) {
93
+ const content = (match[1] || '').trim();
94
+ if (!content || content.length < 10) continue;
95
+
96
+ // Deduplicate by normalized content
97
+ const normalized = content.toLowerCase().replace(/\s+/g, ' ');
98
+ if (seen.has(normalized)) continue;
99
+ seen.add(normalized);
100
+
101
+ candidates.push({
102
+ content,
103
+ type: group.type,
104
+ sector: group.sector,
105
+ confidence: group.confidence,
106
+ });
107
+ }
108
+ }
109
+ }
110
+
111
+ // Sort by confidence (highest first), take top N
112
+ candidates.sort((a, b) => b.confidence - a.confidence);
113
+ const selected = candidates.slice(0, MAX_PER_TURN);
114
+
115
+ // Build memory entries following memory.schema.json
116
+ const now = new Date().toISOString();
117
+ return selected.map((candidate, i) => ({
118
+ id: `mem-${now.split('T')[0]}-${String(i + 1).padStart(3, '0')}`,
119
+ type: candidate.type,
120
+ agent: context.agent || 'unknown',
121
+ content: candidate.content,
122
+ tags: buildTags(candidate.content, context),
123
+ confidence: candidate.confidence,
124
+ sector: candidate.sector,
125
+ tier: candidate.confidence >= 0.7 ? 'hot' : 'warm',
126
+ access_count: 0,
127
+ evidence_count: 1,
128
+ last_accessed: null,
129
+ created_at: now,
130
+ expires_at: null,
131
+ storage_tier: 'daily',
132
+ scope: 'shared',
133
+ }));
134
+ }
135
+
136
+ /**
137
+ * Build tags from memory content and context.
138
+ */
139
+ function buildTags(content, context) {
140
+ const tags = [];
141
+ if (context.agent) tags.push(context.agent);
142
+ if (context.task) tags.push(context.task);
143
+
144
+ // Extract technology keywords
145
+ const techPatterns = /\b(react|next|vue|angular|supabase|postgres|typescript|node|express|tailwind|prisma|zod|jwt|auth|api|css|html)\b/gi;
146
+ const techMatches = content.match(techPatterns) || [];
147
+ for (const tech of [...new Set(techMatches.map(t => t.toLowerCase()))]) {
148
+ if (!tags.includes(tech)) tags.push(tech);
149
+ }
150
+
151
+ return tags.length > 0 ? tags : ['general'];
152
+ }
153
+
154
+ /**
155
+ * Get extraction statistics for monitoring.
156
+ */
157
+ export function getExtractionStats() {
158
+ return {
159
+ maxPerTurn: MAX_PER_TURN,
160
+ patternGroups: EXTRACTION_PATTERNS.length,
161
+ totalPatterns: EXTRACTION_PATTERNS.reduce((sum, g) => sum + g.patterns.length, 0),
162
+ };
163
+ }