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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- If nothing worth saving: respond with exactly NO_UPDATE
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.includes('NO_UPDATE')) {
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
  /**