chati-dev 4.0.11 → 4.1.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.
- package/README.md +14 -9
- package/bin/chati.js +17 -1
- package/framework/agents/build/dev.md +43 -10
- package/framework/agents/discover/brief.md +38 -8
- package/framework/agents/discover/brownfield-wu.md +44 -3
- package/framework/agents/discover/greenfield-wu.md +14 -0
- package/framework/agents/plan/architect-data-engineer.md +278 -0
- package/framework/agents/plan/architect-system.md +174 -0
- package/framework/agents/plan/architect.md +97 -301
- package/framework/agents/plan/detail.md +25 -0
- package/framework/agents/plan/tasks.md +29 -1
- package/framework/agents/plan/ux-brand-architect.md +215 -0
- package/framework/agents/plan/ux-component-engineer.md +289 -0
- package/framework/agents/plan/ux-researcher.md +166 -0
- package/framework/agents/plan/ux.md +126 -1008
- package/framework/agents/quality/qa-implementation.md +121 -22
- package/framework/agents/quality/qa-planning.md +18 -0
- package/framework/config.yaml +15 -4
- package/framework/constitution.md +9 -3
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +59 -3
- package/framework/hooks/constitution-guard.js +67 -1
- package/framework/hooks/license-guard.js +4 -4
- package/framework/hooks/model-governance.js +2 -1
- package/framework/hooks/prism-engine.js +74 -6
- package/framework/hooks/read-protection.js +1 -1
- package/framework/hooks/session-digest.js +159 -7
- package/framework/hooks/settings.json +16 -4
- package/framework/hooks/style-guard.js +134 -0
- package/framework/hooks/undercover-guard.js +220 -0
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +21 -0
- package/framework/intelligence/hooks-performance.md +54 -0
- package/framework/intelligence/memory-layer.md +47 -0
- package/framework/migrations/v4.0-to-v4.1.yaml +165 -0
- package/framework/orchestrator/chati.md +327 -1067
- package/framework/schemas/session.schema.json +1 -1
- package/framework/templates/brandbook-html-tmpl.md +107 -0
- package/framework/templates/session-memory-tmpl.yaml +68 -0
- package/package.json +1 -1
- package/scripts/doctor/checks/agents.js +1 -1
- package/scripts/health-check.js +1 -1
- package/src/autonomy/build-state.js +1 -1
- package/src/autonomy/worktree-manager.js +13 -13
- package/src/config/agent-customizer.js +1 -1
- package/src/config/gemini-hooks-generator.js +149 -6
- package/src/config/ide-configs.js +1 -1
- package/src/context/bracket-tracker.js +25 -4
- package/src/context/engine.js +37 -7
- package/src/context/formatter.js +45 -1
- package/src/context/index.js +3 -3
- package/src/dashboard/data-reader.js +7 -7
- package/src/decision/engine.js +2 -2
- package/src/decision/registry-healer.js +1 -1
- package/src/decision/registry-updater.js +2 -2
- package/src/extensions/loader.js +1 -1
- package/src/gates/g1-planning-complete.js +1 -1
- package/src/gates/g2-qa-planning.js +1 -1
- package/src/gates/g3-implementation.js +4 -4
- package/src/gates/g4-qa-implementation.js +2 -2
- package/src/health/auto-fix.js +3 -3
- package/src/health/engine.js +2 -2
- package/src/installer/brownfield-upgrader.js +3 -3
- package/src/installer/manifest.js +1 -1
- package/src/installer/transaction.js +1 -1
- package/src/installer/validator.js +4 -4
- package/src/intelligence/decision-engine.js +1 -1
- package/src/intelligence/memory-manager.js +1 -1
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +1 -1
- package/src/license/client.js +1 -1
- package/src/license/commands.js +2 -2
- package/src/memory/agent-memory.js +3 -3
- package/src/memory/daily-digest.js +170 -0
- package/src/memory/dream.js +254 -0
- package/src/memory/gotchas.js +2 -2
- package/src/memory/index.js +18 -0
- package/src/memory/magic-docs.js +98 -0
- package/src/memory/memory-extractor.js +163 -0
- package/src/memory/session-digest.js +144 -6
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/cli.js +980 -0
- package/src/orchestrator/handoff-engine.js +31 -4
- package/src/orchestrator/index.js +9 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/preview/detector.js +3 -3
- package/src/preview/launcher.js +2 -2
- package/src/quality/metrics-collector.js +1 -1
- package/src/quality/test-runner.js +2 -4
- package/src/scanning/density-scanner.js +51 -0
- package/src/scanning/env-scanner.js +97 -0
- package/src/scanning/index.js +7 -0
- package/src/scanning/leakage-scanner.js +54 -0
- package/src/scanning/placeholder-scanner.js +40 -0
- package/src/scanning/security-scanner.js +94 -0
- package/src/security/bash-security.js +335 -0
- package/src/security/index.js +9 -0
- package/src/telemetry/config.js +3 -3
- package/src/telemetry/sender.js +1 -1
- package/src/terminal/cli-registry.js +1 -1
- package/src/terminal/cost-tracker.js +1 -2
- package/src/terminal/prompt-builder.js +11 -27
- package/src/terminal/rate-limiter.js +1 -2
- package/src/terminal/run-agent.js +3 -3
- package/src/terminal/spawner.js +41 -4
- package/src/upgrade/checker.js +2 -2
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +1 -1
- package/src/utils/file-lock.js +3 -3
- package/src/utils/provider-limits.js +35 -4
- package/src/wizard/i18n.js +4 -4
- package/src/wizard/index.js +2 -1
- 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
|
+
}
|
package/src/memory/gotchas.js
CHANGED
|
@@ -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
|
}
|
package/src/memory/index.js
CHANGED
|
@@ -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
|
+
}
|