metame-cli 1.2.1 → 1.2.2
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/index.js +152 -1
- package/package.json +1 -1
- package/scripts/distill.js +220 -19
- package/scripts/schema.js +9 -0
package/index.js
CHANGED
|
@@ -294,8 +294,86 @@ if (!isKnownUser) {
|
|
|
294
294
|
console.log("🆕 User Unknown: Injecting Deep Genesis Protocol...");
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// ---------------------------------------------------------
|
|
298
|
+
// 4.5 MIRROR INJECTION (Phase C — metacognition observation)
|
|
299
|
+
// ---------------------------------------------------------
|
|
300
|
+
let mirrorLine = '';
|
|
301
|
+
try {
|
|
302
|
+
if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
|
|
303
|
+
const brainDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
304
|
+
|
|
305
|
+
// Check quiet mode
|
|
306
|
+
const quietUntil = brainDoc.growth && brainDoc.growth.quiet_until;
|
|
307
|
+
const isQuiet = quietUntil && new Date(quietUntil).getTime() > Date.now();
|
|
308
|
+
|
|
309
|
+
// Check mirror enabled (default: true)
|
|
310
|
+
const mirrorEnabled = !(brainDoc.growth && brainDoc.growth.mirror_enabled === false);
|
|
311
|
+
|
|
312
|
+
if (!isQuiet && mirrorEnabled && brainDoc.growth && Array.isArray(brainDoc.growth.patterns)) {
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
const COOLDOWN_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
|
|
315
|
+
|
|
316
|
+
// Find a pattern that hasn't been surfaced in 14 days
|
|
317
|
+
const candidate = brainDoc.growth.patterns.find(p => {
|
|
318
|
+
if (!p.surfaced) return true;
|
|
319
|
+
return (now - new Date(p.surfaced).getTime()) > COOLDOWN_MS;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (candidate) {
|
|
323
|
+
mirrorLine = `\n[MetaMe observation: ${candidate.summary} 不要主动提起,只在用户自然提到相关话题时温和回应。]\n`;
|
|
324
|
+
|
|
325
|
+
// Mark as surfaced
|
|
326
|
+
candidate.surfaced = new Date().toISOString().slice(0, 10);
|
|
327
|
+
fs.writeFileSync(BRAIN_FILE, yaml.dump(brainDoc, { lineWidth: -1 }), 'utf8');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Non-fatal
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------
|
|
336
|
+
// 4.6 REFLECTION PROMPT (Phase C — conditional, NOT static)
|
|
337
|
+
// ---------------------------------------------------------
|
|
338
|
+
// Only inject when trigger conditions are met at startup.
|
|
339
|
+
// This ensures reflections don't fire every session.
|
|
340
|
+
let reflectionLine = '';
|
|
341
|
+
try {
|
|
342
|
+
if (isKnownUser && fs.existsSync(BRAIN_FILE)) {
|
|
343
|
+
const refDoc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
344
|
+
|
|
345
|
+
// Check quiet mode
|
|
346
|
+
const quietUntil = refDoc.growth && refDoc.growth.quiet_until;
|
|
347
|
+
const isQuietForRef = quietUntil && new Date(quietUntil).getTime() > Date.now();
|
|
348
|
+
|
|
349
|
+
if (!isQuietForRef) {
|
|
350
|
+
const distillCount = (refDoc.evolution && refDoc.evolution.distill_count) || 0;
|
|
351
|
+
const zoneHistory = (refDoc.growth && refDoc.growth.zone_history) || [];
|
|
352
|
+
|
|
353
|
+
// Trigger 1: Every 7th session
|
|
354
|
+
const trigger7th = distillCount > 0 && distillCount % 7 === 0;
|
|
355
|
+
|
|
356
|
+
// Trigger 2: Three consecutive comfort-zone sessions
|
|
357
|
+
const lastThree = zoneHistory.slice(-3);
|
|
358
|
+
const triggerComfort = lastThree.length === 3 && lastThree.every(z => z === 'C');
|
|
359
|
+
|
|
360
|
+
if (trigger7th || triggerComfort) {
|
|
361
|
+
let hint = '';
|
|
362
|
+
if (triggerComfort) {
|
|
363
|
+
hint = '连续几次都在熟悉领域。如果用户在session结束时自然停顿,可以温和地问:🪞 准备好探索拉伸区了吗?';
|
|
364
|
+
} else {
|
|
365
|
+
hint = '这是第' + distillCount + '次session。如果session自然结束,可以附加一句:🪞 一个词形容这次session的感受?';
|
|
366
|
+
}
|
|
367
|
+
reflectionLine = `\n[MetaMe reflection: ${hint} 只在session即将结束时说一次。如果用户没回应就不要追问。]\n`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
// Non-fatal
|
|
373
|
+
}
|
|
374
|
+
|
|
297
375
|
// Prepend the new Protocol to the top
|
|
298
|
-
const newContent = finalProtocol + "\n" + fileContent;
|
|
376
|
+
const newContent = finalProtocol + mirrorLine + reflectionLine + "\n" + fileContent;
|
|
299
377
|
fs.writeFileSync(PROJECT_FILE, newContent, 'utf8');
|
|
300
378
|
|
|
301
379
|
console.log("🔮 MetaMe: Link Established.");
|
|
@@ -412,6 +490,79 @@ if (isSetTrait) {
|
|
|
412
490
|
process.exit(0);
|
|
413
491
|
}
|
|
414
492
|
|
|
493
|
+
// ---------------------------------------------------------
|
|
494
|
+
// 5.5 METACOGNITION CONTROL COMMANDS (Phase C)
|
|
495
|
+
// ---------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
// metame quiet — silence mirror + reflections for 48 hours
|
|
498
|
+
const isQuiet = process.argv.includes('quiet');
|
|
499
|
+
if (isQuiet) {
|
|
500
|
+
try {
|
|
501
|
+
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
502
|
+
if (!doc.growth) doc.growth = {};
|
|
503
|
+
doc.growth.quiet_until = new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString();
|
|
504
|
+
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
505
|
+
console.log("🤫 MetaMe: Mirror & reflections silenced for 48 hours.");
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error("❌ Error:", e.message);
|
|
508
|
+
}
|
|
509
|
+
process.exit(0);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// metame insights — show detected patterns
|
|
513
|
+
const isInsights = process.argv.includes('insights');
|
|
514
|
+
if (isInsights) {
|
|
515
|
+
try {
|
|
516
|
+
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
517
|
+
const patterns = (doc.growth && doc.growth.patterns) || [];
|
|
518
|
+
const zoneHistory = (doc.growth && doc.growth.zone_history) || [];
|
|
519
|
+
|
|
520
|
+
if (patterns.length === 0) {
|
|
521
|
+
console.log("🔍 MetaMe: No patterns detected yet. Keep using MetaMe and patterns will emerge after ~5 sessions.");
|
|
522
|
+
} else {
|
|
523
|
+
console.log("🪞 MetaMe Insights:\n");
|
|
524
|
+
patterns.forEach((p, i) => {
|
|
525
|
+
const icon = p.type === 'avoidance' ? '⚠️' : p.type === 'growth' ? '🌱' : p.type === 'energy' ? '⚡' : '🔄';
|
|
526
|
+
console.log(` ${icon} [${p.type}] ${p.summary} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
|
|
527
|
+
console.log(` Detected: ${p.detected}${p.surfaced ? `, Last shown: ${p.surfaced}` : ''}`);
|
|
528
|
+
});
|
|
529
|
+
if (zoneHistory.length > 0) {
|
|
530
|
+
console.log(`\n 📊 Recent zone history: ${zoneHistory.join(' → ')}`);
|
|
531
|
+
console.log(` (C=Comfort, S=Stretch, P=Panic)`);
|
|
532
|
+
}
|
|
533
|
+
const answered = (doc.growth && doc.growth.reflections_answered) || 0;
|
|
534
|
+
const skipped = (doc.growth && doc.growth.reflections_skipped) || 0;
|
|
535
|
+
if (answered + skipped > 0) {
|
|
536
|
+
console.log(`\n 💭 Reflections: ${answered} answered, ${skipped} skipped`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} catch (e) {
|
|
540
|
+
console.error("❌ Error:", e.message);
|
|
541
|
+
}
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// metame mirror on/off — toggle mirror injection
|
|
546
|
+
const isMirror = process.argv.includes('mirror');
|
|
547
|
+
if (isMirror) {
|
|
548
|
+
const mirrorIndex = process.argv.indexOf('mirror');
|
|
549
|
+
const toggle = process.argv[mirrorIndex + 1];
|
|
550
|
+
if (toggle !== 'on' && toggle !== 'off') {
|
|
551
|
+
console.error("❌ Usage: metame mirror on|off");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
556
|
+
if (!doc.growth) doc.growth = {};
|
|
557
|
+
doc.growth.mirror_enabled = (toggle === 'on');
|
|
558
|
+
fs.writeFileSync(BRAIN_FILE, yaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
559
|
+
console.log(`🪞 MetaMe: Mirror ${toggle === 'on' ? 'enabled' : 'disabled'}.`);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
console.error("❌ Error:", e.message);
|
|
562
|
+
}
|
|
563
|
+
process.exit(0);
|
|
564
|
+
}
|
|
565
|
+
|
|
415
566
|
// ---------------------------------------------------------
|
|
416
567
|
// ---------------------------------------------------------
|
|
417
568
|
// 6. SAFETY GUARD: RECURSION PREVENTION (v2)
|
package/package.json
CHANGED
package/scripts/distill.js
CHANGED
|
@@ -29,24 +29,24 @@ const { loadPending, savePending, upsertPending, getPromotable, removePromoted }
|
|
|
29
29
|
function distill() {
|
|
30
30
|
// 1. Check if buffer exists and has content
|
|
31
31
|
if (!fs.existsSync(BUFFER_FILE)) {
|
|
32
|
-
return { updated: false, summary: 'No signals to process.' };
|
|
32
|
+
return { updated: false, behavior: null, summary: 'No signals to process.' };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const raw = fs.readFileSync(BUFFER_FILE, 'utf8').trim();
|
|
36
36
|
if (!raw) {
|
|
37
|
-
return { updated: false, summary: 'Empty buffer.' };
|
|
37
|
+
return { updated: false, behavior: null, summary: 'Empty buffer.' };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const lines = raw.split('\n').filter(l => l.trim());
|
|
41
41
|
if (lines.length === 0) {
|
|
42
|
-
return { updated: false, summary: 'No signals to process.' };
|
|
42
|
+
return { updated: false, behavior: null, summary: 'No signals to process.' };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// 2. Prevent concurrent distillation
|
|
46
46
|
if (fs.existsSync(LOCK_FILE)) {
|
|
47
47
|
const lockAge = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
48
48
|
if (lockAge < 120000) { // 2 min timeout
|
|
49
|
-
return { updated: false, summary: 'Distillation already in progress.' };
|
|
49
|
+
return { updated: false, behavior: null, summary: 'Distillation already in progress.' };
|
|
50
50
|
}
|
|
51
51
|
// Stale lock, remove it
|
|
52
52
|
fs.unlinkSync(LOCK_FILE);
|
|
@@ -71,7 +71,7 @@ function distill() {
|
|
|
71
71
|
|
|
72
72
|
if (signals.length === 0) {
|
|
73
73
|
cleanup();
|
|
74
|
-
return { updated: false, summary: 'No valid signals.' };
|
|
74
|
+
return { updated: false, behavior: null, summary: 'No valid signals.' };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// 4. Read current profile
|
|
@@ -138,7 +138,30 @@ _source:
|
|
|
138
138
|
context.focus: "我现在在做API重构"
|
|
139
139
|
\`\`\`
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
BEHAVIORAL PATTERN DETECTION (Phase C):
|
|
142
|
+
In addition to cognitive traits, analyze the messages for behavioral patterns in THIS session.
|
|
143
|
+
Output a _behavior block with these fields (use null if not enough signal):
|
|
144
|
+
decision_pattern: premature_closure | exploratory | iterative | null
|
|
145
|
+
cognitive_load: low | medium | high | null
|
|
146
|
+
zone: comfort | stretch | panic | null
|
|
147
|
+
avoidance_topics: [] # topics mentioned but not acted on
|
|
148
|
+
emotional_response: analytical | blame_external | blame_self | withdrawal | null
|
|
149
|
+
topics: [] # main topics discussed (max 5 keywords)
|
|
150
|
+
|
|
151
|
+
Example _behavior block:
|
|
152
|
+
\`\`\`yaml
|
|
153
|
+
_behavior:
|
|
154
|
+
decision_pattern: iterative
|
|
155
|
+
cognitive_load: medium
|
|
156
|
+
zone: stretch
|
|
157
|
+
avoidance_topics: ["testing"]
|
|
158
|
+
emotional_response: analytical
|
|
159
|
+
topics: ["rust", "error-handling"]
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
IMPORTANT: _behavior is ALWAYS output, even if no profile updates. If there are no profile updates but behavior was detected, output ONLY the _behavior block (do NOT output NO_UPDATE in that case).
|
|
163
|
+
|
|
164
|
+
If nothing worth saving AND no behavior detected: respond with exactly NO_UPDATE
|
|
142
165
|
Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
143
166
|
|
|
144
167
|
// 6. Call Claude in print mode with haiku
|
|
@@ -158,28 +181,28 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
158
181
|
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
159
182
|
const isTimeout = err.killed || (err.signal === 'SIGTERM');
|
|
160
183
|
if (isTimeout) {
|
|
161
|
-
return { updated: false, summary: 'Skipped — API too slow. Will retry next launch.' };
|
|
184
|
+
return { updated: false, behavior: null, summary: 'Skipped — API too slow. Will retry next launch.' };
|
|
162
185
|
}
|
|
163
|
-
return { updated: false, summary: 'Skipped — Claude not available. Will retry next launch.' };
|
|
186
|
+
return { updated: false, behavior: null, summary: 'Skipped — Claude not available. Will retry next launch.' };
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
// 7. Parse result
|
|
167
|
-
if (!result || result
|
|
190
|
+
if (!result || result === 'NO_UPDATE') {
|
|
168
191
|
cleanup();
|
|
169
|
-
return { updated: false, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
|
|
192
|
+
return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
// Extract YAML block from response — require explicit code block, no fallback
|
|
173
196
|
const yamlMatch = result.match(/```yaml\n([\s\S]*?)```/) || result.match(/```\n([\s\S]*?)```/);
|
|
174
197
|
if (!yamlMatch) {
|
|
175
198
|
cleanup();
|
|
176
|
-
return { updated: false, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
|
|
199
|
+
return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
|
|
177
200
|
}
|
|
178
201
|
const yamlContent = yamlMatch[1].trim();
|
|
179
202
|
|
|
180
203
|
if (!yamlContent) {
|
|
181
204
|
cleanup();
|
|
182
|
-
return { updated: false, summary: 'Distiller returned empty result.' };
|
|
205
|
+
return { updated: false, behavior: null, summary: 'Distiller returned empty result.' };
|
|
183
206
|
}
|
|
184
207
|
|
|
185
208
|
// 8. Validate against schema + merge into profile
|
|
@@ -188,14 +211,24 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
188
211
|
const updates = yaml.load(yamlContent);
|
|
189
212
|
if (!updates || typeof updates !== 'object') {
|
|
190
213
|
cleanup();
|
|
191
|
-
return { updated: false, summary: 'Distiller returned invalid data.' };
|
|
214
|
+
return { updated: false, behavior: null, summary: 'Distiller returned invalid data.' };
|
|
192
215
|
}
|
|
193
216
|
|
|
217
|
+
// Extract _behavior block before filtering (it's not a profile field)
|
|
218
|
+
const behavior = updates._behavior || null;
|
|
219
|
+
delete updates._behavior;
|
|
220
|
+
|
|
194
221
|
// Schema whitelist filter: drop any keys not in schema or locked
|
|
195
222
|
const filtered = filterBySchema(updates);
|
|
196
|
-
if (Object.keys(filtered).length === 0) {
|
|
223
|
+
if (Object.keys(filtered).length === 0 && !behavior) {
|
|
197
224
|
cleanup();
|
|
198
|
-
return { updated: false, summary: `Analyzed ${signals.length} messages — all extracted fields rejected by schema.` };
|
|
225
|
+
return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — all extracted fields rejected by schema.` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// If only behavior detected but no profile updates
|
|
229
|
+
if (Object.keys(filtered).length === 0 && behavior) {
|
|
230
|
+
cleanup();
|
|
231
|
+
return { updated: false, behavior, signalCount: signals.length, summary: `Analyzed ${signals.length} messages — behavior logged, no profile changes.` };
|
|
199
232
|
}
|
|
200
233
|
|
|
201
234
|
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
@@ -250,7 +283,7 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
250
283
|
if (tokens > TOKEN_BUDGET) {
|
|
251
284
|
// Step 3: Reject write entirely, keep previous version
|
|
252
285
|
cleanup();
|
|
253
|
-
return { updated: false, summary: `Profile too large (${tokens} tokens > ${TOKEN_BUDGET}). Write rejected to prevent bloat.` };
|
|
286
|
+
return { updated: false, behavior, signalCount: signals.length, summary: `Profile too large (${tokens} tokens > ${TOKEN_BUDGET}). Write rejected to prevent bloat.` };
|
|
254
287
|
}
|
|
255
288
|
|
|
256
289
|
fs.writeFileSync(BRAIN_FILE, restored, 'utf8');
|
|
@@ -258,17 +291,19 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
258
291
|
cleanup();
|
|
259
292
|
return {
|
|
260
293
|
updated: true,
|
|
294
|
+
behavior,
|
|
295
|
+
signalCount: signals.length,
|
|
261
296
|
summary: `${Object.keys(filtered).length} new trait${Object.keys(filtered).length > 1 ? 's' : ''} absorbed. (${tokens} tokens)`
|
|
262
297
|
};
|
|
263
298
|
|
|
264
299
|
} catch (err) {
|
|
265
300
|
cleanup();
|
|
266
|
-
return { updated: false, summary: `Profile merge failed: ${err.message}` };
|
|
301
|
+
return { updated: false, behavior: null, summary: `Profile merge failed: ${err.message}` };
|
|
267
302
|
}
|
|
268
303
|
|
|
269
304
|
} catch (err) {
|
|
270
305
|
cleanup();
|
|
271
|
-
return { updated: false, summary: `Distillation error: ${err.message}` };
|
|
306
|
+
return { updated: false, behavior: null, summary: `Distillation error: ${err.message}` };
|
|
272
307
|
}
|
|
273
308
|
}
|
|
274
309
|
|
|
@@ -481,12 +516,178 @@ function cleanup() {
|
|
|
481
516
|
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
482
517
|
}
|
|
483
518
|
|
|
519
|
+
// ---------------------------------------------------------
|
|
520
|
+
// SESSION LOG — records behavioral patterns per distill cycle
|
|
521
|
+
// ---------------------------------------------------------
|
|
522
|
+
const SESSION_LOG_FILE = path.join(HOME, '.metame', 'session_log.yaml');
|
|
523
|
+
const MAX_SESSION_LOG = 30;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Write a session entry to session_log.yaml.
|
|
527
|
+
* @param {object} behavior - The _behavior block from Haiku
|
|
528
|
+
* @param {number} signalCount - Number of signals processed
|
|
529
|
+
*/
|
|
530
|
+
function writeSessionLog(behavior, signalCount) {
|
|
531
|
+
if (!behavior) return;
|
|
532
|
+
|
|
533
|
+
const yaml = require('js-yaml');
|
|
534
|
+
let log = { sessions: [] };
|
|
535
|
+
try {
|
|
536
|
+
if (fs.existsSync(SESSION_LOG_FILE)) {
|
|
537
|
+
log = yaml.load(fs.readFileSync(SESSION_LOG_FILE, 'utf8')) || { sessions: [] };
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
log = { sessions: [] };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!Array.isArray(log.sessions)) log.sessions = [];
|
|
544
|
+
|
|
545
|
+
const entry = {
|
|
546
|
+
ts: new Date().toISOString().slice(0, 10),
|
|
547
|
+
topics: behavior.topics || [],
|
|
548
|
+
zone: behavior.zone || null,
|
|
549
|
+
decision_pattern: behavior.decision_pattern || null,
|
|
550
|
+
cognitive_load: behavior.cognitive_load || null,
|
|
551
|
+
emotional_response: behavior.emotional_response || null,
|
|
552
|
+
avoidance: behavior.avoidance_topics || [],
|
|
553
|
+
signal_count: signalCount,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
log.sessions.push(entry);
|
|
557
|
+
|
|
558
|
+
// FIFO: keep only most recent entries
|
|
559
|
+
if (log.sessions.length > MAX_SESSION_LOG) {
|
|
560
|
+
log.sessions = log.sessions.slice(-MAX_SESSION_LOG);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fs.writeFileSync(SESSION_LOG_FILE, yaml.dump(log, { lineWidth: -1 }), 'utf8');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ---------------------------------------------------------
|
|
567
|
+
// PATTERN DETECTION — every 5th distill, analyze session_log
|
|
568
|
+
// ---------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Detect repeated behavioral patterns from session history.
|
|
572
|
+
* Called when distill_count % 5 === 0 and there are enough sessions.
|
|
573
|
+
* Writes results to profile growth.patterns (max 3).
|
|
574
|
+
*/
|
|
575
|
+
function detectPatterns() {
|
|
576
|
+
const yaml = require('js-yaml');
|
|
577
|
+
|
|
578
|
+
// Read session log
|
|
579
|
+
if (!fs.existsSync(SESSION_LOG_FILE)) return;
|
|
580
|
+
const log = yaml.load(fs.readFileSync(SESSION_LOG_FILE, 'utf8'));
|
|
581
|
+
if (!log || !Array.isArray(log.sessions) || log.sessions.length < 5) return;
|
|
582
|
+
|
|
583
|
+
// Read current profile to check distill_count
|
|
584
|
+
if (!fs.existsSync(BRAIN_FILE)) return;
|
|
585
|
+
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8'));
|
|
586
|
+
if (!profile) return;
|
|
587
|
+
|
|
588
|
+
const distillCount = (profile.evolution && profile.evolution.distill_count) || 0;
|
|
589
|
+
if (distillCount % 5 !== 0 || distillCount === 0) return;
|
|
590
|
+
|
|
591
|
+
// Take last 20 sessions
|
|
592
|
+
const recent = log.sessions.slice(-20);
|
|
593
|
+
const sessionSummary = recent.map((s, i) =>
|
|
594
|
+
`${i + 1}. [${s.ts}] topics=${(s.topics || []).join(',')} zone=${s.zone || '?'} decision=${s.decision_pattern || '?'} load=${s.cognitive_load || '?'} avoidance=[${(s.avoidance || []).join(',')}]`
|
|
595
|
+
).join('\n');
|
|
596
|
+
|
|
597
|
+
const patternPrompt = `You are a metacognition pattern detector. Analyze these ${recent.length} session summaries and find repeated behavioral patterns.
|
|
598
|
+
|
|
599
|
+
SESSION HISTORY:
|
|
600
|
+
${sessionSummary}
|
|
601
|
+
|
|
602
|
+
Find at most 2 patterns from these categories:
|
|
603
|
+
1. Avoidance: topics mentioned repeatedly but never acted on
|
|
604
|
+
2. Energy: what task types correlate with high/low cognitive load
|
|
605
|
+
3. Zone: consecutive comfort zone? frequent panic?
|
|
606
|
+
4. Growth: areas where user went from asking questions to giving commands (mastery signal)
|
|
607
|
+
|
|
608
|
+
RULES:
|
|
609
|
+
- Only report patterns with confidence > 0.7 (based on frequency/consistency)
|
|
610
|
+
- Each pattern must appear in at least 3 sessions to count
|
|
611
|
+
- Be specific and concise (one sentence per pattern)
|
|
612
|
+
|
|
613
|
+
OUTPUT FORMAT — respond with ONLY a YAML code block:
|
|
614
|
+
\`\`\`yaml
|
|
615
|
+
patterns:
|
|
616
|
+
- type: avoidance|energy|zone|growth
|
|
617
|
+
summary: "one sentence description"
|
|
618
|
+
confidence: 0.7-1.0
|
|
619
|
+
\`\`\`
|
|
620
|
+
|
|
621
|
+
If no clear patterns found: respond with exactly NO_PATTERNS`;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const result = execSync(
|
|
625
|
+
`claude -p --model haiku`,
|
|
626
|
+
{
|
|
627
|
+
input: patternPrompt,
|
|
628
|
+
encoding: 'utf8',
|
|
629
|
+
timeout: 30000,
|
|
630
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
631
|
+
}
|
|
632
|
+
).trim();
|
|
633
|
+
|
|
634
|
+
if (!result || result.includes('NO_PATTERNS')) return;
|
|
635
|
+
|
|
636
|
+
const yamlMatch = result.match(/```yaml\n([\s\S]*?)```/) || result.match(/```\n([\s\S]*?)```/);
|
|
637
|
+
if (!yamlMatch) return;
|
|
638
|
+
|
|
639
|
+
const parsed = yaml.load(yamlMatch[1].trim());
|
|
640
|
+
if (!parsed || !Array.isArray(parsed.patterns)) return;
|
|
641
|
+
|
|
642
|
+
// Validate and cap at 3 patterns
|
|
643
|
+
const validated = parsed.patterns
|
|
644
|
+
.filter(p => p.type && p.summary && p.confidence >= 0.7)
|
|
645
|
+
.slice(0, 3)
|
|
646
|
+
.map(p => ({
|
|
647
|
+
type: p.type,
|
|
648
|
+
summary: p.summary,
|
|
649
|
+
detected: new Date().toISOString().slice(0, 10),
|
|
650
|
+
surfaced: null,
|
|
651
|
+
confidence: p.confidence,
|
|
652
|
+
}));
|
|
653
|
+
|
|
654
|
+
if (validated.length === 0) return;
|
|
655
|
+
|
|
656
|
+
// Write to profile growth.patterns
|
|
657
|
+
const rawProfile = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
658
|
+
const freshProfile = yaml.load(rawProfile) || {};
|
|
659
|
+
if (!freshProfile.growth) freshProfile.growth = {};
|
|
660
|
+
freshProfile.growth.patterns = validated;
|
|
661
|
+
|
|
662
|
+
// Also update zone_history from recent sessions
|
|
663
|
+
const zoneHistory = recent.slice(-10)
|
|
664
|
+
.map(s => {
|
|
665
|
+
if (s.zone === 'comfort') return 'C';
|
|
666
|
+
if (s.zone === 'stretch') return 'S';
|
|
667
|
+
if (s.zone === 'panic') return 'P';
|
|
668
|
+
return '?';
|
|
669
|
+
});
|
|
670
|
+
freshProfile.growth.zone_history = zoneHistory;
|
|
671
|
+
|
|
672
|
+
fs.writeFileSync(BRAIN_FILE, yaml.dump(freshProfile, { lineWidth: -1 }), 'utf8');
|
|
673
|
+
|
|
674
|
+
} catch {
|
|
675
|
+
// Non-fatal — pattern detection failure shouldn't break anything
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
484
679
|
// Export for use in index.js
|
|
485
|
-
module.exports = { distill };
|
|
680
|
+
module.exports = { distill, writeSessionLog, detectPatterns };
|
|
486
681
|
|
|
487
682
|
// Also allow direct execution
|
|
488
683
|
if (require.main === module) {
|
|
489
684
|
const result = distill();
|
|
685
|
+
// Write session log if behavior was detected
|
|
686
|
+
if (result.behavior) {
|
|
687
|
+
writeSessionLog(result.behavior, result.signalCount || 0);
|
|
688
|
+
}
|
|
689
|
+
// Run pattern detection (only triggers every 5th distill)
|
|
690
|
+
detectPatterns();
|
|
490
691
|
if (result.updated) {
|
|
491
692
|
console.log(`🧠 ${result.summary}`);
|
|
492
693
|
} else {
|
package/scripts/schema.js
CHANGED
|
@@ -68,6 +68,15 @@ const SCHEMA = {
|
|
|
68
68
|
'evolution.distill_count': { tier: 'T5', type: 'number' },
|
|
69
69
|
'evolution.recent_changes': { tier: 'T5', type: 'array', maxItems: 5 },
|
|
70
70
|
'evolution.auto_distill': { tier: 'T5', type: 'array', maxItems: 10 },
|
|
71
|
+
|
|
72
|
+
// === T5: Growth (metacognition, system-managed) ===
|
|
73
|
+
'growth.patterns': { tier: 'T5', type: 'array', maxItems: 3 },
|
|
74
|
+
'growth.zone_history': { tier: 'T5', type: 'array', maxItems: 10 },
|
|
75
|
+
'growth.reflections_answered': { tier: 'T5', type: 'number' },
|
|
76
|
+
'growth.reflections_skipped': { tier: 'T5', type: 'number' },
|
|
77
|
+
'growth.last_reflection': { tier: 'T5', type: 'string' },
|
|
78
|
+
'growth.quiet_until': { tier: 'T5', type: 'string' },
|
|
79
|
+
'growth.mirror_enabled': { tier: 'T5', type: 'boolean' },
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
/**
|