metame-cli 1.4.31 → 1.4.33
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 +42 -19
- package/index.js +237 -149
- package/package.json +3 -2
- package/scripts/bin/dispatch_to +92 -0
- package/scripts/daemon-claude-engine.js +5 -2
- package/scripts/daemon-runtime-lifecycle.js +4 -2
- package/scripts/daemon-task-scheduler.js +28 -3
- package/scripts/daemon.js +40 -3
- package/scripts/docs/agent-guide.md +50 -0
- package/scripts/docs/file-transfer.md +32 -0
- package/scripts/feishu-adapter.js +6 -2
- package/scripts/memory-extract.js +2 -2
- package/scripts/memory-gc.js +2 -2
- package/scripts/memory-write.js +0 -1
- package/scripts/memory.js +1 -1
- package/scripts/platform.js +172 -0
- package/scripts/reliability-core.test.js +15 -3
- package/scripts/schema.js +43 -10
- package/scripts/skill-evolution.test.js +7 -1
- package/scripts/sync-readme.js +64 -0
- package/scripts/templates/default-global-claude.md +19 -0
- package/scripts/utils.test.js +12 -5
- package/scripts/migrate-v2.js +0 -112
- package/scripts/self-reflect.js +0 -285
package/scripts/schema.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Tiers:
|
|
11
11
|
* T1 — Identity (LOCKED, never auto-modify)
|
|
12
|
-
* T2 —
|
|
12
|
+
* T2 — Soul (LOCKED, 6-dimension personality model)
|
|
13
13
|
* T3 — Preferences (auto-writable, needs confidence)
|
|
14
14
|
* T5 — Evolution (system-managed, strict limits)
|
|
15
15
|
*
|
|
@@ -23,14 +23,47 @@ const SCHEMA = {
|
|
|
23
23
|
'identity.role': { tier: 'T1', type: 'string', locked: false },
|
|
24
24
|
'identity.locale': { tier: 'T1', type: 'string', locked: true },
|
|
25
25
|
|
|
26
|
-
// === T2:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// === T2: Soul (6-Dimension Model, LOCKED) ===
|
|
27
|
+
|
|
28
|
+
// Dim 1: Values (Schwartz Value Theory)
|
|
29
|
+
'soul.values.primary': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
|
|
30
|
+
'soul.values.secondary': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
|
|
31
|
+
'soul.values.anti_value': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
|
|
32
|
+
|
|
33
|
+
// Dim 2: Drive (Self-Determination Theory)
|
|
34
|
+
'soul.drive.primary_need': { tier: 'T2', type: 'enum', locked: true,
|
|
35
|
+
values: ['autonomy', 'mastery', 'connection', 'impact', 'security', 'novelty', 'meaning'] },
|
|
36
|
+
'soul.drive.flow_trigger': { tier: 'T2', type: 'string', locked: true, maxChars: 60 },
|
|
37
|
+
'soul.drive.north_star.aspiration': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
|
|
38
|
+
'soul.drive.north_star.realistic': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
|
|
39
|
+
|
|
40
|
+
// Dim 3: Cognition Style (Jung + Kahneman)
|
|
41
|
+
'soul.cognition_style.thinking_axis': { tier: 'T2', type: 'enum', locked: true,
|
|
42
|
+
values: ['systematic', 'intuitive', 'dialectical'] },
|
|
43
|
+
'soul.cognition_style.learning_mode': { tier: 'T2', type: 'enum', locked: true,
|
|
44
|
+
values: ['by_doing', 'by_modeling', 'by_abstracting', 'by_debating', 'by_reflecting'] },
|
|
45
|
+
'soul.cognition_style.complexity_appetite': { tier: 'T2', type: 'enum', locked: true,
|
|
46
|
+
values: ['reductionist', 'comfortable_with_ambiguity', 'complexity_seeker'] },
|
|
47
|
+
|
|
48
|
+
// Dim 4: Stress & Shadow (Jung Shadow + Resilience Theory)
|
|
49
|
+
'soul.stress.crisis_reflex': { tier: 'T2', type: 'enum', locked: true,
|
|
50
|
+
values: ['fight', 'flight', 'freeze', 'analyze'] },
|
|
51
|
+
'soul.stress.shadow': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
|
|
52
|
+
'soul.stress.recovery_pattern': { tier: 'T2', type: 'enum', locked: true,
|
|
53
|
+
values: ['solitude', 'social_support', 'physical_action', 'intellectual_distraction', 'sleep_reset'] },
|
|
54
|
+
|
|
55
|
+
// Dim 5: Relational (Attachment Theory + FIRO-B)
|
|
56
|
+
'soul.relational.trust_formation': { tier: 'T2', type: 'enum', locked: true,
|
|
57
|
+
values: ['competence_first', 'character_first', 'shared_experience', 'slow_incremental'] },
|
|
58
|
+
'soul.relational.conflict_style': { tier: 'T2', type: 'enum', locked: true,
|
|
59
|
+
values: ['direct_confrontation', 'strategic_avoidance', 'diplomatic_mediation', 'withdrawal'] },
|
|
60
|
+
'soul.relational.authority_stance': { tier: 'T2', type: 'enum', locked: true,
|
|
61
|
+
values: ['challenge_authority', 'respect_hierarchy', 'pragmatic_compliance', 'build_own_authority'] },
|
|
62
|
+
|
|
63
|
+
// Dim 6: Identity Narrative (McAdams Narrative Identity)
|
|
64
|
+
'soul.identity_narrative.self_in_one_line': { tier: 'T2', type: 'string', locked: true, maxChars: 100 },
|
|
65
|
+
'soul.identity_narrative.core_contradiction': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
|
|
66
|
+
'soul.identity_narrative.feared_self': { tier: 'T2', type: 'string', locked: true, maxChars: 60 },
|
|
34
67
|
|
|
35
68
|
// === T3: Preferences ===
|
|
36
69
|
'preferences.code_style': { tier: 'T3', type: 'enum', values: ['concise', 'verbose', 'documented'] },
|
|
@@ -80,7 +113,7 @@ const SCHEMA = {
|
|
|
80
113
|
|
|
81
114
|
/**
|
|
82
115
|
* Check if a dotted key matches the schema.
|
|
83
|
-
* Supports wildcard entries
|
|
116
|
+
* Supports wildcard entries (e.g. 'namespace.*') and exact dotted keys.
|
|
84
117
|
*/
|
|
85
118
|
function hasKey(key) {
|
|
86
119
|
if (SCHEMA[key]) return true;
|
|
@@ -12,10 +12,16 @@ function mkHome() {
|
|
|
12
12
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'metame-skill-evo-'));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function homeEnv(home) {
|
|
16
|
+
return process.platform === 'win32'
|
|
17
|
+
? { HOME: home, USERPROFILE: home }
|
|
18
|
+
: { HOME: home };
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
function runWithHome(home, code) {
|
|
16
22
|
return execFileSync(process.execPath, ['-e', code], {
|
|
17
23
|
cwd: ROOT,
|
|
18
|
-
env: { ...process.env,
|
|
24
|
+
env: { ...process.env, ...homeEnv(home) },
|
|
19
25
|
encoding: 'utf8',
|
|
20
26
|
});
|
|
21
27
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* sync-readme.js — Translate README.md (English) → README中文版.md
|
|
6
|
+
*
|
|
7
|
+
* Usage: node scripts/sync-readme.js
|
|
8
|
+
* Or: npm run sync:readme
|
|
9
|
+
*
|
|
10
|
+
* Uses claude CLI to translate. English README is the source of truth.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
const SRC = path.join(ROOT, 'README.md');
|
|
19
|
+
const DST = path.join(ROOT, 'README中文版.md');
|
|
20
|
+
|
|
21
|
+
const english = fs.readFileSync(SRC, 'utf8');
|
|
22
|
+
|
|
23
|
+
const prompt = `You are a professional translator. Translate the following GitHub README from English to Chinese (简体中文).
|
|
24
|
+
|
|
25
|
+
Rules:
|
|
26
|
+
- Keep ALL markdown formatting, links, code blocks, HTML tags, and badges EXACTLY as-is
|
|
27
|
+
- Keep all technical terms, CLI commands, file paths, and config examples in English
|
|
28
|
+
- Translate prose, descriptions, and comments naturally — not word-by-word
|
|
29
|
+
- The first tagline should be: > **住在你电脑里的数字分身。**
|
|
30
|
+
- Change "Your machine, your data" to "不上云。你的机器,你的数据。"
|
|
31
|
+
- Keep the <p align="center"> header block unchanged
|
|
32
|
+
- Output ONLY the translated markdown, no extra explanation
|
|
33
|
+
|
|
34
|
+
Here is the README to translate:
|
|
35
|
+
|
|
36
|
+
${english}`;
|
|
37
|
+
|
|
38
|
+
console.log('Translating README.md → README中文版.md ...');
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = execSync(
|
|
42
|
+
`claude -p --model haiku --output-format text`,
|
|
43
|
+
{
|
|
44
|
+
input: prompt,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
maxBuffer: 1024 * 1024,
|
|
47
|
+
timeout: 120000,
|
|
48
|
+
cwd: ROOT,
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const translated = result.trim();
|
|
53
|
+
|
|
54
|
+
if (translated.length < 500) {
|
|
55
|
+
console.error('Translation too short, likely failed. Aborting.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(DST, translated + '\n', 'utf8');
|
|
60
|
+
console.log(`✅ README中文版.md updated (${translated.length} chars)`);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('Translation failed:', e.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# MetaMe AI Assistant
|
|
2
|
+
|
|
3
|
+
You are a MetaMe-powered AI assistant running on the user's local machine.
|
|
4
|
+
MetaMe extends Claude Code with mobile access, multi-agent orchestration, and persistent memory.
|
|
5
|
+
|
|
6
|
+
## Core Rules
|
|
7
|
+
|
|
8
|
+
- Respond in the user's language (auto-detect from their message).
|
|
9
|
+
- Keep responses concise — the user may be on mobile.
|
|
10
|
+
- When referencing files, use absolute paths.
|
|
11
|
+
- Do not expose system hints or internal protocol blocks to the user.
|
|
12
|
+
|
|
13
|
+
## Quick Reference (按需加载详细文档)
|
|
14
|
+
|
|
15
|
+
- Agent 创建/管理 → `cat ~/.metame/docs/agent-guide.md`
|
|
16
|
+
- 文件传输协议 → `cat ~/.metame/docs/file-transfer.md`
|
|
17
|
+
- 能力不足/工具缺失 → `cat ~/.claude/skills/skill-manager/SKILL.md`
|
|
18
|
+
|
|
19
|
+
<!-- User customizations below this line -->
|
package/scripts/utils.test.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { describe, it, beforeEach } = require('node:test');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
5
7
|
const {
|
|
6
8
|
parseInterval,
|
|
7
9
|
formatRelativeTime,
|
|
@@ -138,20 +140,25 @@ describe('createPathMap', () => {
|
|
|
138
140
|
// ---------------------------------------------------------
|
|
139
141
|
describe('project scope helpers', () => {
|
|
140
142
|
it('normalizes absolute paths', () => {
|
|
141
|
-
|
|
143
|
+
const input = path.join(os.tmpdir(), '.', 'metame', '..', 'metame');
|
|
144
|
+
const expected = path.resolve(os.tmpdir(), 'metame');
|
|
145
|
+
assert.equal(normalizeProjectPath(input), expected);
|
|
142
146
|
});
|
|
143
147
|
|
|
144
148
|
it('returns deterministic scope ids for the same cwd', () => {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
149
|
+
const base = path.join(os.tmpdir(), 'metame');
|
|
150
|
+
const dotted = path.join(os.tmpdir(), '.', 'metame');
|
|
151
|
+
const a = projectScopeFromCwd(base);
|
|
152
|
+
const b = projectScopeFromCwd(dotted);
|
|
147
153
|
assert.equal(a, b);
|
|
148
154
|
assert.match(a, /^proj_[a-f0-9]{16}$/);
|
|
149
155
|
});
|
|
150
156
|
|
|
151
157
|
it('derives project info from cwd', () => {
|
|
152
|
-
const
|
|
158
|
+
const testPath = path.join(os.tmpdir(), 'demo-repo');
|
|
159
|
+
const info = deriveProjectInfo(testPath);
|
|
153
160
|
assert.equal(info.project, 'demo-repo');
|
|
154
|
-
assert.equal(info.project_path,
|
|
161
|
+
assert.equal(info.project_path, path.resolve(testPath));
|
|
155
162
|
assert.match(info.project_id, /^proj_[a-f0-9]{16}$/);
|
|
156
163
|
});
|
|
157
164
|
});
|
package/scripts/migrate-v2.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MetaMe Profile Migration: v1 → v2
|
|
5
|
-
*
|
|
6
|
-
* Maps old structure to v2 schema:
|
|
7
|
-
* - status.focus → context.focus
|
|
8
|
-
* - status.language → preferences.language_mix (best guess)
|
|
9
|
-
* - Ensures all v2 sections exist with defaults
|
|
10
|
-
* - Preserves all existing data and LOCKED comments
|
|
11
|
-
*
|
|
12
|
-
* Usage: node migrate-v2.js [--dry-run]
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const os = require('os');
|
|
18
|
-
|
|
19
|
-
const BRAIN_FILE = path.join(os.homedir(), '.claude_profile.yaml');
|
|
20
|
-
const BACKUP_SUFFIX = '.v1.backup';
|
|
21
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
22
|
-
|
|
23
|
-
function migrate() {
|
|
24
|
-
if (!fs.existsSync(BRAIN_FILE)) {
|
|
25
|
-
console.log('No profile found. Nothing to migrate.');
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const yaml = require('js-yaml');
|
|
30
|
-
const rawContent = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
31
|
-
const profile = yaml.load(rawContent) || {};
|
|
32
|
-
|
|
33
|
-
// Check if already v2 (has context section)
|
|
34
|
-
if (profile.context && profile.context.focus !== undefined) {
|
|
35
|
-
console.log('Profile already appears to be v2. Skipping migration.');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
console.log('Migrating profile from v1 to v2...');
|
|
40
|
-
|
|
41
|
-
// --- Backup ---
|
|
42
|
-
if (!DRY_RUN) {
|
|
43
|
-
const backupPath = BRAIN_FILE + BACKUP_SUFFIX;
|
|
44
|
-
fs.writeFileSync(backupPath, rawContent, 'utf8');
|
|
45
|
-
console.log(` Backup saved to: ${backupPath}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// --- Migration rules ---
|
|
49
|
-
|
|
50
|
-
// 1. status.focus → context.focus
|
|
51
|
-
if (profile.status && profile.status.focus) {
|
|
52
|
-
if (!profile.context) profile.context = {};
|
|
53
|
-
profile.context.focus = profile.status.focus;
|
|
54
|
-
profile.context.focus_since = new Date().toISOString().slice(0, 10);
|
|
55
|
-
delete profile.status.focus;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 2. status.language → status.language (keep, it's in schema)
|
|
59
|
-
// No change needed, status.language is valid in v2
|
|
60
|
-
|
|
61
|
-
// 3. Clean up empty status object
|
|
62
|
-
if (profile.status && Object.keys(profile.status).length === 0) {
|
|
63
|
-
delete profile.status;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 4. Ensure context section exists with defaults
|
|
67
|
-
if (!profile.context) profile.context = {};
|
|
68
|
-
if (profile.context.focus === undefined) profile.context.focus = null;
|
|
69
|
-
if (profile.context.focus_since === undefined) profile.context.focus_since = null;
|
|
70
|
-
if (profile.context.active_projects === undefined) profile.context.active_projects = [];
|
|
71
|
-
if (profile.context.blockers === undefined) profile.context.blockers = [];
|
|
72
|
-
if (profile.context.energy === undefined) profile.context.energy = null;
|
|
73
|
-
|
|
74
|
-
// 5. Ensure evolution section exists
|
|
75
|
-
if (!profile.evolution) profile.evolution = {};
|
|
76
|
-
if (profile.evolution.last_distill === undefined) profile.evolution.last_distill = null;
|
|
77
|
-
if (profile.evolution.distill_count === undefined) profile.evolution.distill_count = 0;
|
|
78
|
-
if (profile.evolution.recent_changes === undefined) profile.evolution.recent_changes = [];
|
|
79
|
-
|
|
80
|
-
// 6. Ensure preferences section exists (don't overwrite existing values)
|
|
81
|
-
if (!profile.preferences) profile.preferences = {};
|
|
82
|
-
|
|
83
|
-
// --- Output ---
|
|
84
|
-
const dumped = yaml.dump(profile, { lineWidth: -1 });
|
|
85
|
-
|
|
86
|
-
// Restore LOCKED comments from original
|
|
87
|
-
const lockedLines = rawContent.split('\n').filter(l => l.includes('# [LOCKED]'));
|
|
88
|
-
let restored = dumped;
|
|
89
|
-
for (const lockedLine of lockedLines) {
|
|
90
|
-
const match = lockedLine.match(/^\s*([\w_]+)\s*:\s*(.+?)\s+(#.+)$/);
|
|
91
|
-
if (match) {
|
|
92
|
-
const key = match[1];
|
|
93
|
-
const comment = match[3];
|
|
94
|
-
// Find the corresponding line in dumped output and append comment
|
|
95
|
-
restored = restored.replace(
|
|
96
|
-
new RegExp(`^(\\s*${key}\\s*:.+)$`, 'm'),
|
|
97
|
-
(line) => line.includes('#') ? line : `${line} ${comment}`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (DRY_RUN) {
|
|
103
|
-
console.log('\n--- DRY RUN (would write): ---');
|
|
104
|
-
console.log(restored);
|
|
105
|
-
console.log('--- END DRY RUN ---');
|
|
106
|
-
} else {
|
|
107
|
-
fs.writeFileSync(BRAIN_FILE, restored, 'utf8');
|
|
108
|
-
console.log(' Migration complete. Profile is now v2.');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
migrate();
|
package/scripts/self-reflect.js
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* self-reflect.js — Daily Self-Reflection Task
|
|
5
|
-
*
|
|
6
|
-
* Scans correction/metacognitive signals from the past 7 days,
|
|
7
|
-
* aggregates "where did the AI get it wrong", and writes a brief
|
|
8
|
-
* self-critique pattern into growth.patterns in ~/.claude_profile.yaml.
|
|
9
|
-
*
|
|
10
|
-
* Also distills correction signals into lessons/ SOP markdown files.
|
|
11
|
-
*
|
|
12
|
-
* Heartbeat: nightly at 23:00, require_idle, non-blocking.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const os = require('os');
|
|
20
|
-
const { callHaiku, buildDistillEnv } = require('./providers');
|
|
21
|
-
const { writeBrainFileSafe } = require('./utils');
|
|
22
|
-
|
|
23
|
-
const HOME = os.homedir();
|
|
24
|
-
const SIGNAL_FILE = path.join(HOME, '.metame', 'raw_signals.jsonl');
|
|
25
|
-
const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
26
|
-
const LOCK_FILE = path.join(HOME, '.metame', 'self-reflect.lock');
|
|
27
|
-
const LESSONS_DIR = path.join(HOME, '.metame', 'memory', 'lessons');
|
|
28
|
-
const WINDOW_DAYS = 7;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Distill correction signals into reusable SOP markdown files.
|
|
32
|
-
* Each run produces at most one lesson file per unique slug.
|
|
33
|
-
* Returns the number of lesson files actually written.
|
|
34
|
-
*
|
|
35
|
-
* @param {Array} signals - all recent signals (will filter to 'correction' type internally)
|
|
36
|
-
* @param {string} lessonsDir - absolute path where lesson .md files are written
|
|
37
|
-
*/
|
|
38
|
-
async function generateLessons(signals, lessonsDir) {
|
|
39
|
-
// Only process correction signals that carry explicit feedback
|
|
40
|
-
const corrections = signals.filter(s => s.type === 'correction' && s.feedback);
|
|
41
|
-
if (corrections.length < 2) {
|
|
42
|
-
console.log(`[self-reflect] Only ${corrections.length} correction signal(s) with feedback, skipping lessons.`);
|
|
43
|
-
return 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
fs.mkdirSync(lessonsDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
const correctionText = corrections
|
|
49
|
-
.slice(-15) // cap to avoid prompt bloat
|
|
50
|
-
.map(c => `- Prompt: ${(c.prompt || '').slice(0, 100)}\n Feedback: ${(c.feedback || '').slice(0, 150)}`)
|
|
51
|
-
.join('\n');
|
|
52
|
-
|
|
53
|
-
const prompt = `You are distilling correction signals into a reusable SOP for an AI assistant.
|
|
54
|
-
|
|
55
|
-
Corrections (JSON):
|
|
56
|
-
${correctionText}
|
|
57
|
-
|
|
58
|
-
Generate ONE actionable lesson in this JSON format:
|
|
59
|
-
{
|
|
60
|
-
"title": "简短标题(中文,10字以内)",
|
|
61
|
-
"slug": "kebab-case-english-slug",
|
|
62
|
-
"content": "## 问题\\n...\\n## 根因\\n...\\n## 操作手册\\n1. ...\\n2. ...\\n3. ..."
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
Rules: content must be in 中文, concrete and actionable, 100-300 chars total.
|
|
66
|
-
Only output the JSON object, no explanation.`;
|
|
67
|
-
|
|
68
|
-
let distillEnv = {};
|
|
69
|
-
try { distillEnv = buildDistillEnv(); } catch {}
|
|
70
|
-
|
|
71
|
-
let result;
|
|
72
|
-
try {
|
|
73
|
-
result = await Promise.race([
|
|
74
|
-
callHaiku(prompt, distillEnv, 60000),
|
|
75
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 90000)),
|
|
76
|
-
]);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.log(`[self-reflect] generateLessons Haiku call failed: ${e.message}`);
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let lesson;
|
|
83
|
-
try {
|
|
84
|
-
const cleaned = result.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
85
|
-
lesson = JSON.parse(cleaned);
|
|
86
|
-
if (!lesson.title || !lesson.slug || !lesson.content) throw new Error('missing fields');
|
|
87
|
-
} catch (e) {
|
|
88
|
-
console.log(`[self-reflect] Failed to parse lesson JSON: ${e.message}`);
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Sanitize slug: only lowercase alphanumeric and hyphens
|
|
93
|
-
const slug = (lesson.slug || '').toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
94
|
-
if (!slug) {
|
|
95
|
-
console.log('[self-reflect] generateLessons: empty slug, skipping');
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Prevent duplicates: skip if any existing file already uses this slug
|
|
100
|
-
const existing = fs.readdirSync(lessonsDir).filter(f => f.endsWith(`-${slug}.md`));
|
|
101
|
-
if (existing.length > 0) {
|
|
102
|
-
console.log(`[self-reflect] Lesson '${slug}' already exists (${existing[0]}), skipping.`);
|
|
103
|
-
return 0;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
107
|
-
const filename = `${today}-${slug}.md`;
|
|
108
|
-
const filepath = path.join(lessonsDir, filename);
|
|
109
|
-
|
|
110
|
-
const fileContent = `---
|
|
111
|
-
date: ${today}
|
|
112
|
-
source: self-reflect
|
|
113
|
-
corrections: ${corrections.length}
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
# ${lesson.title}
|
|
117
|
-
|
|
118
|
-
${lesson.content}
|
|
119
|
-
`;
|
|
120
|
-
|
|
121
|
-
fs.writeFileSync(filepath, fileContent, 'utf8');
|
|
122
|
-
console.log(`[self-reflect] Lesson written: ${filepath}`);
|
|
123
|
-
return 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function run() {
|
|
127
|
-
// Atomic lock
|
|
128
|
-
let lockFd;
|
|
129
|
-
try {
|
|
130
|
-
lockFd = fs.openSync(LOCK_FILE, 'wx');
|
|
131
|
-
fs.writeSync(lockFd, process.pid.toString());
|
|
132
|
-
fs.closeSync(lockFd);
|
|
133
|
-
} catch (e) {
|
|
134
|
-
if (e.code === 'EEXIST') {
|
|
135
|
-
const age = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
136
|
-
if (age < 300000) { console.log('[self-reflect] Already running.'); return; }
|
|
137
|
-
fs.unlinkSync(LOCK_FILE);
|
|
138
|
-
try {
|
|
139
|
-
lockFd = fs.openSync(LOCK_FILE, 'wx');
|
|
140
|
-
} catch {
|
|
141
|
-
// Another process acquired the lock
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
fs.writeSync(lockFd, process.pid.toString());
|
|
145
|
-
fs.closeSync(lockFd);
|
|
146
|
-
} else throw e;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
// Read signals from last WINDOW_DAYS days
|
|
151
|
-
if (!fs.existsSync(SIGNAL_FILE)) {
|
|
152
|
-
console.log('[self-reflect] No signal file, skipping.');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const cutoff = Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
157
|
-
const lines = fs.readFileSync(SIGNAL_FILE, 'utf8').trim().split('\n').filter(Boolean);
|
|
158
|
-
const recentSignals = lines
|
|
159
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
160
|
-
.filter(s => s && s.ts && new Date(s.ts).getTime() > cutoff);
|
|
161
|
-
|
|
162
|
-
// Filter to correction + metacognitive signals only
|
|
163
|
-
const correctionSignals = recentSignals.filter(s =>
|
|
164
|
-
s.type === 'correction' || s.type === 'metacognitive'
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
if (correctionSignals.length < 2) {
|
|
168
|
-
console.log(`[self-reflect] Only ${correctionSignals.length} correction signals this week, skipping.`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Read current profile for context
|
|
173
|
-
let currentPatterns = '';
|
|
174
|
-
try {
|
|
175
|
-
const yaml = require('js-yaml');
|
|
176
|
-
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
177
|
-
const existing = (profile.growth && profile.growth.patterns) || [];
|
|
178
|
-
if (existing.length > 0) {
|
|
179
|
-
currentPatterns = `Current growth.patterns (avoid repeating):\n${existing.map(p => `- ${p}`).join('\n')}\n\n`;
|
|
180
|
-
}
|
|
181
|
-
} catch { /* non-fatal */ }
|
|
182
|
-
|
|
183
|
-
const signalText = correctionSignals
|
|
184
|
-
.slice(-20) // cap at 20 signals
|
|
185
|
-
.map((s, i) => `${i + 1}. [${s.type}] "${s.prompt}"`)
|
|
186
|
-
.join('\n');
|
|
187
|
-
|
|
188
|
-
const prompt = `你是一个AI自我审视引擎。分析以下用户纠正/元认知信号,找出AI(即你)**系统性**犯错的模式。
|
|
189
|
-
|
|
190
|
-
${currentPatterns}用户纠正信号(最近7天):
|
|
191
|
-
${signalText}
|
|
192
|
-
|
|
193
|
-
任务:找出1-2条AI的系统性问题(不是偶发错误),例如:
|
|
194
|
-
- "经常过度简化用户的技术问题,忽略背景细节"
|
|
195
|
-
- "倾向于在用户还没说完就开始行动,导致方向偏差"
|
|
196
|
-
- "在不确定时倾向于肯定用户,而非直接说不知道"
|
|
197
|
-
|
|
198
|
-
输出格式(JSON数组,最多2条,每条≤40字中文):
|
|
199
|
-
["模式1描述", "模式2描述"]
|
|
200
|
-
|
|
201
|
-
注意:
|
|
202
|
-
- 只输出有充分证据支持的系统性模式
|
|
203
|
-
- 如果证据不足,输出 []
|
|
204
|
-
- 只输出JSON,不要解释`;
|
|
205
|
-
|
|
206
|
-
let distillEnv = {};
|
|
207
|
-
try { distillEnv = buildDistillEnv(); } catch {}
|
|
208
|
-
|
|
209
|
-
let result;
|
|
210
|
-
try {
|
|
211
|
-
result = await Promise.race([
|
|
212
|
-
callHaiku(prompt, distillEnv, 60000),
|
|
213
|
-
// outer safety net in case callHaiku's internal timeout doesn't propagate
|
|
214
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 90000)),
|
|
215
|
-
]);
|
|
216
|
-
} catch (e) {
|
|
217
|
-
console.log(`[self-reflect] Haiku call failed: ${e.message}`);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Parse result
|
|
222
|
-
let patterns = [];
|
|
223
|
-
try {
|
|
224
|
-
const cleaned = result.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
225
|
-
const parsed = JSON.parse(cleaned);
|
|
226
|
-
if (Array.isArray(parsed)) {
|
|
227
|
-
patterns = parsed.filter(p => typeof p === 'string' && p.length > 5 && p.length <= 80);
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
console.log('[self-reflect] Failed to parse Haiku output.');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// === Generate lessons/ from correction signals (independent of patterns result) ===
|
|
235
|
-
try {
|
|
236
|
-
const lessonsCount = await generateLessons(recentSignals, LESSONS_DIR);
|
|
237
|
-
if (lessonsCount > 0) {
|
|
238
|
-
console.log(`[self-reflect] Generated ${lessonsCount} lesson(s) in ${LESSONS_DIR}`);
|
|
239
|
-
}
|
|
240
|
-
} catch (e) {
|
|
241
|
-
console.log(`[self-reflect] generateLessons failed (non-fatal): ${e.message}`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (patterns.length === 0) {
|
|
245
|
-
console.log('[self-reflect] No patterns found this week.');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Merge into growth.patterns (cap at 3, keep newest)
|
|
250
|
-
try {
|
|
251
|
-
const yaml = require('js-yaml');
|
|
252
|
-
const raw = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
253
|
-
const profile = yaml.load(raw) || {};
|
|
254
|
-
if (!profile.growth) profile.growth = {};
|
|
255
|
-
const existing = Array.isArray(profile.growth.patterns) ? profile.growth.patterns : [];
|
|
256
|
-
// Add new patterns, deduplicate, cap at 3 newest
|
|
257
|
-
const merged = [...existing, ...patterns]
|
|
258
|
-
.filter((p, i, arr) => arr.indexOf(p) === i)
|
|
259
|
-
.slice(-3);
|
|
260
|
-
profile.growth.patterns = merged;
|
|
261
|
-
profile.growth.last_reflection = new Date().toISOString().slice(0, 10);
|
|
262
|
-
|
|
263
|
-
// Preserve locked lines (simple approach: only update growth section)
|
|
264
|
-
const dumped = yaml.dump(profile, { lineWidth: -1 });
|
|
265
|
-
await writeBrainFileSafe(dumped);
|
|
266
|
-
console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.patterns: ${patterns.join(' | ')}`);
|
|
267
|
-
} catch (e) {
|
|
268
|
-
console.log(`[self-reflect] Failed to write profile: ${e.message}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
} finally {
|
|
272
|
-
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (require.main === module) {
|
|
277
|
-
run().then(() => {
|
|
278
|
-
console.log('✅ self-reflect complete');
|
|
279
|
-
}).catch(e => {
|
|
280
|
-
console.error(`[self-reflect] Fatal: ${e.message}`);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
module.exports = { run };
|