metame-cli 1.3.0 → 1.3.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/README.md +40 -3
- package/index.js +2 -9
- package/package.json +1 -1
- package/scripts/daemon.js +132 -7
- package/scripts/distill.js +52 -1
- package/scripts/schema.js +2 -0
package/README.md
CHANGED
|
@@ -20,9 +20,10 @@ It is not a memory system; it is a **Cognitive Mirror** .
|
|
|
20
20
|
|
|
21
21
|
* **🧠 Global Brain (`~/.claude_profile.yaml`):** A single, portable source of truth — your identity, cognitive traits, and preferences travel with you across every project.
|
|
22
22
|
* **🧬 Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** — silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** — `!metame evolve` for explicit teaching; (3) **Confidence gates** — strong directives ("always"/"以后一律") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
|
|
23
|
-
* **🤝 Dynamic Handshake:** The "Canary Test." Claude must address you by your **Codename** in the first sentence. If it doesn't, the link is broken.
|
|
24
23
|
* **🛡️ Auto-Lock:** Mark any value with `# [LOCKED]` — treated as a constitution, never auto-modified.
|
|
24
|
+
* **🪞 Metacognition Layer (v1.3):** MetaMe now observes *how* you think, not just *what* you say. Behavioral pattern detection runs inside the existing Haiku distill call (zero extra cost). It tracks decision patterns, cognitive load, comfort zones, and avoidance topics across sessions. When persistent patterns emerge, MetaMe injects a one-line mirror observation — e.g., *"You tend to avoid testing until forced"* — with a 14-day cooldown per pattern. Conditional reflection prompts appear only when triggered (every 7th distill or 3x consecutive comfort zone). All injection logic runs in Node.js; Claude receives only pre-decided directives, never rules to self-evaluate.
|
|
25
25
|
* **📱 Remote Claude Code (v1.3):** Full Claude Code from your phone via Telegram or Feishu (Lark). Stateful sessions with `--resume` — same conversation history, tool use, and file editing as your terminal. Interactive buttons for project/session picking, directory browser, and macOS launchd auto-start.
|
|
26
|
+
* **🔄 Workflow Engine (v1.3):** Define multi-step skill chains as heartbeat tasks. Each workflow runs in a single Claude Code session via `--resume`, so step outputs flow as context to the next step. Example: `deep-research` → `tech-writing` → `wechat-publisher` — fully automated content pipeline.
|
|
26
27
|
|
|
27
28
|
## 🛠 Prerequisites
|
|
28
29
|
|
|
@@ -105,8 +106,21 @@ metame set-trait status.focus "Learning Rust"
|
|
|
105
106
|
metame evolve "I prefer functional programming patterns"
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
**Episodic memory (keyframe, not full log):** MetaMe is not a memory system, but it captures two types of experiential "keyframes" that pure personality traits can't replace:
|
|
110
|
+
|
|
111
|
+
* **Anti-patterns** (`context.anti_patterns`, max 5): Cross-project failure lessons — e.g., *"Promise.all rejects all on single failure, use Promise.allSettled"*. Auto-expires after 60 days. Prevents the AI from repeating the same mistakes across sessions.
|
|
112
|
+
* **Milestones** (`context.milestones`, max 3): Recent completed landmarks — e.g., *"MetaMe v1.3 published"*. Provides continuity so Claude knows where you left off without you having to recap.
|
|
113
|
+
|
|
108
114
|
**Anti-bias safeguards:** single observations ≠ traits, contradictions are tracked not overwritten, pending traits expire after 30 days, context fields auto-clear on staleness.
|
|
109
115
|
|
|
116
|
+
**Metacognition controls:**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
metame quiet # Silence mirror observations & reflections for 48h
|
|
120
|
+
metame insights # Show detected behavioral patterns
|
|
121
|
+
metame mirror on|off # Toggle mirror injection
|
|
122
|
+
```
|
|
123
|
+
|
|
110
124
|
### Remote Claude Code — Telegram & Feishu (v1.3)
|
|
111
125
|
|
|
112
126
|
Full Claude Code from your phone — stateful sessions with conversation history, tool use, and file editing. Supports both Telegram and Feishu (Lark).
|
|
@@ -168,6 +182,7 @@ Each chat gets a persistent session via `claude -p --resume <session-id>`. This
|
|
|
168
182
|
| `/run <name>` | Run a task immediately |
|
|
169
183
|
| `/budget` | Today's token usage |
|
|
170
184
|
| `/quiet` | Silence mirror/reflections for 48h |
|
|
185
|
+
| `/reload` | Manually reload daemon.yaml (also auto-reloads on file change) |
|
|
171
186
|
|
|
172
187
|
**Heartbeat Tasks:**
|
|
173
188
|
|
|
@@ -188,6 +203,30 @@ heartbeat:
|
|
|
188
203
|
* `type: "script"`: Run a local script directly instead of `claude -p`.
|
|
189
204
|
* `notify: true`: Push results to Telegram/Feishu.
|
|
190
205
|
|
|
206
|
+
**Workflow tasks** (multi-step skill chains):
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
heartbeat:
|
|
210
|
+
tasks:
|
|
211
|
+
- name: "daily-wechat"
|
|
212
|
+
type: "workflow"
|
|
213
|
+
interval: "24h"
|
|
214
|
+
model: "sonnet"
|
|
215
|
+
notify: true
|
|
216
|
+
steps:
|
|
217
|
+
- skill: "deep-research"
|
|
218
|
+
prompt: "Today's top 3 AI news stories"
|
|
219
|
+
- skill: "tech-writing"
|
|
220
|
+
prompt: "Write a WeChat article based on the research above"
|
|
221
|
+
- skill: "wechat-publisher"
|
|
222
|
+
prompt: "Publish the article"
|
|
223
|
+
optional: true
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Each step runs in the same Claude Code session. Step outputs automatically become context for the next step. Set `optional: true` on steps that may fail without aborting the workflow.
|
|
227
|
+
|
|
228
|
+
**Auto-reload:** The daemon watches `daemon.yaml` for changes. When Claude (or you) edits the config file, the daemon automatically reloads — no restart or `/reload` needed. A notification is pushed to confirm.
|
|
229
|
+
|
|
191
230
|
**Token efficiency:**
|
|
192
231
|
|
|
193
232
|
* Polling, slash commands, directory browsing: **zero tokens**
|
|
@@ -356,8 +395,6 @@ You might worry: *"Does this eat up my context window?"*
|
|
|
356
395
|
**Q: Does this replace `CLAUDE.md`?**
|
|
357
396
|
A: No. It *prepends* its meta-cognitive protocol to your existing `CLAUDE.md`. Your project-specific notes remain intact.
|
|
358
397
|
|
|
359
|
-
**Q: What if Claude stops calling me by my nickname?**
|
|
360
|
-
A: This is the "Canary Test." It means the context window has been compressed or the file link is broken. Run `/compact` in Claude or restart `metame` to fix it.
|
|
361
398
|
|
|
362
399
|
**Q: Is my data sent to a third party?**
|
|
363
400
|
A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passes text to the official Claude Code tool.
|
package/index.js
CHANGED
|
@@ -208,20 +208,13 @@ const CORE_PROTOCOL = `
|
|
|
208
208
|
---
|
|
209
209
|
## 🧠 SYSTEM KERNEL: SHADOW_MODE (Active)
|
|
210
210
|
|
|
211
|
-
**1. THE
|
|
212
|
-
* **SOURCE:** Check \`$HOME/.claude_profile.yaml\`.
|
|
213
|
-
* **RULE:** Look for \`identity.nickname\`.
|
|
214
|
-
* **IF FOUND:** You MUST address the user by that nickname in the very first sentence.
|
|
215
|
-
* **IF NULL/MISSING:** Address user as "User" and immediately trigger Genesis to fix it.
|
|
216
|
-
* **SIGNAL:** This nickname is the "Canary Test". If you omit it, the user assumes the system is broken.
|
|
217
|
-
|
|
218
|
-
**2. THE BRAIN (Source of Truth):**
|
|
211
|
+
**1. THE BRAIN (Source of Truth):**
|
|
219
212
|
* **FILE:** \`$HOME/.claude_profile.yaml\`
|
|
220
213
|
* **RULE:** This file is your Ground Truth. If context is compressed (/compact), **RE-READ** this file immediately.
|
|
221
214
|
* **AUTO-LOCK:** Never edit lines marked \`# [LOCKED]\` (Core Values).
|
|
222
215
|
* **EVOLUTION:** Silently overwrite outdated status/focus.
|
|
223
216
|
|
|
224
|
-
**
|
|
217
|
+
**2. EVOLUTION MECHANISM (Manual Sync):**
|
|
225
218
|
* **PHILOSOPHY:** You respect the User's flow. You do NOT interrupt.
|
|
226
219
|
* **TOOLS:**
|
|
227
220
|
1. **Log Insight:** \`!metame evolve "Insight"\` (For additive knowledge).
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -211,6 +211,11 @@ function executeTask(task, config) {
|
|
|
211
211
|
return { success: true, output: '(skipped — no activity)', skipped: true };
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Workflow tasks: multi-step skill chain via --resume session
|
|
215
|
+
if (task.type === 'workflow') {
|
|
216
|
+
return executeWorkflow(task, config);
|
|
217
|
+
}
|
|
218
|
+
|
|
214
219
|
// Script tasks: run a local script directly (e.g. distill.js), no claude -p
|
|
215
220
|
if (task.type === 'script') {
|
|
216
221
|
log('INFO', `Executing script task: ${task.name} → ${task.command}`);
|
|
@@ -251,11 +256,12 @@ function executeTask(task, config) {
|
|
|
251
256
|
}
|
|
252
257
|
const fullPrompt = preamble + taskPrompt;
|
|
253
258
|
|
|
259
|
+
const allowedArgs = (task.allowedTools || []).map(t => `--allowedTools ${t}`).join(' ');
|
|
254
260
|
log('INFO', `Executing task: ${task.name} (model: ${model})`);
|
|
255
261
|
|
|
256
262
|
try {
|
|
257
263
|
const output = execSync(
|
|
258
|
-
`claude -p --model ${model}`,
|
|
264
|
+
`claude -p --model ${model}${allowedArgs ? ' ' + allowedArgs : ''}`,
|
|
259
265
|
{
|
|
260
266
|
input: fullPrompt,
|
|
261
267
|
encoding: 'utf8',
|
|
@@ -307,6 +313,70 @@ function parseInterval(str) {
|
|
|
307
313
|
}
|
|
308
314
|
}
|
|
309
315
|
|
|
316
|
+
// ---------------------------------------------------------
|
|
317
|
+
// WORKFLOW EXECUTION (multi-step skill chain via --resume)
|
|
318
|
+
// ---------------------------------------------------------
|
|
319
|
+
function executeWorkflow(task, config) {
|
|
320
|
+
const state = loadState();
|
|
321
|
+
if (!checkBudget(config, state)) {
|
|
322
|
+
log('WARN', `Budget exceeded, skipping workflow: ${task.name}`);
|
|
323
|
+
return { success: false, error: 'budget_exceeded', output: '' };
|
|
324
|
+
}
|
|
325
|
+
const precheck = checkPrecondition(task);
|
|
326
|
+
if (!precheck.pass) {
|
|
327
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'skipped', output_preview: 'Precondition not met' };
|
|
328
|
+
saveState(state);
|
|
329
|
+
return { success: true, output: '(skipped)', skipped: true };
|
|
330
|
+
}
|
|
331
|
+
const steps = task.steps || [];
|
|
332
|
+
if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
|
|
333
|
+
|
|
334
|
+
const model = task.model || 'sonnet';
|
|
335
|
+
const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
|
|
336
|
+
const sessionId = crypto.randomUUID();
|
|
337
|
+
const outputs = [];
|
|
338
|
+
let totalTokens = 0;
|
|
339
|
+
const allowed = task.allowedTools || [];
|
|
340
|
+
|
|
341
|
+
log('INFO', `Workflow ${task.name}: ${steps.length} steps, session ${sessionId.slice(0, 8)}`);
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < steps.length; i++) {
|
|
344
|
+
const step = steps[i];
|
|
345
|
+
let prompt = (step.skill ? `/${step.skill} ` : '') + (step.prompt || '');
|
|
346
|
+
if (i === 0 && precheck.context) prompt += `\n\n相关数据:\n\`\`\`\n${precheck.context}\n\`\`\``;
|
|
347
|
+
const args = ['-p', '--model', model];
|
|
348
|
+
for (const tool of allowed) args.push('--allowedTools', tool);
|
|
349
|
+
args.push(i === 0 ? '--session-id' : '--resume', sessionId);
|
|
350
|
+
|
|
351
|
+
log('INFO', `Workflow ${task.name} step ${i + 1}/${steps.length}: ${step.skill || 'prompt'}`);
|
|
352
|
+
try {
|
|
353
|
+
const output = execSync(`claude ${args.join(' ')}`, {
|
|
354
|
+
input: prompt, encoding: 'utf8', timeout: step.timeout || 300000, maxBuffer: 5 * 1024 * 1024, cwd,
|
|
355
|
+
}).trim();
|
|
356
|
+
const tk = Math.ceil((prompt.length + output.length) / 4);
|
|
357
|
+
totalTokens += tk;
|
|
358
|
+
outputs.push({ step: i + 1, skill: step.skill || null, output: output.slice(0, 500), tokens: tk });
|
|
359
|
+
log('INFO', `Workflow ${task.name} step ${i + 1} done (${tk} tokens)`);
|
|
360
|
+
if (!checkBudget(config, loadState())) { log('WARN', 'Budget exceeded mid-workflow'); break; }
|
|
361
|
+
} catch (e) {
|
|
362
|
+
log('ERROR', `Workflow ${task.name} step ${i + 1} failed: ${e.message.slice(0, 200)}`);
|
|
363
|
+
outputs.push({ step: i + 1, skill: step.skill || null, error: e.message.slice(0, 200) });
|
|
364
|
+
if (!step.optional) {
|
|
365
|
+
recordTokens(loadState(), totalTokens);
|
|
366
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'error', error: `Step ${i + 1} failed`, steps_completed: i, steps_total: steps.length };
|
|
367
|
+
saveState(state);
|
|
368
|
+
return { success: false, error: `Step ${i + 1} failed`, output: outputs.map(o => `Step ${o.step}: ${o.error ? 'FAILED' : 'OK'}`).join('\n'), tokens: totalTokens };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
recordTokens(loadState(), totalTokens);
|
|
373
|
+
const lastOk = [...outputs].reverse().find(o => !o.error);
|
|
374
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'success', output_preview: (lastOk ? lastOk.output : '').slice(0, 200), steps_completed: outputs.filter(o => !o.error).length, steps_total: steps.length };
|
|
375
|
+
saveState(state);
|
|
376
|
+
log('INFO', `Workflow ${task.name} done: ${outputs.filter(o => !o.error).length}/${steps.length} steps (${totalTokens} tokens)`);
|
|
377
|
+
return { success: true, output: outputs.map(o => `Step ${o.step} (${o.skill || 'prompt'}): ${o.error ? 'FAILED' : 'OK'}`).join('\n') + '\n\n' + (lastOk ? lastOk.output : ''), tokens: totalTokens };
|
|
378
|
+
}
|
|
379
|
+
|
|
310
380
|
// ---------------------------------------------------------
|
|
311
381
|
// HEARTBEAT SCHEDULER
|
|
312
382
|
// ---------------------------------------------------------
|
|
@@ -709,6 +779,20 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
709
779
|
return;
|
|
710
780
|
}
|
|
711
781
|
|
|
782
|
+
if (text === '/reload') {
|
|
783
|
+
if (global._metameReload) {
|
|
784
|
+
const r = global._metameReload();
|
|
785
|
+
if (r.success) {
|
|
786
|
+
await bot.sendMessage(chatId, `✅ Config reloaded. ${r.tasks} heartbeat tasks active.`);
|
|
787
|
+
} else {
|
|
788
|
+
await bot.sendMessage(chatId, `❌ Reload failed: ${r.error}`);
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
await bot.sendMessage(chatId, '❌ Reload not available (daemon not fully started).');
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
712
796
|
if (text.startsWith('/')) {
|
|
713
797
|
await bot.sendMessage(chatId, [
|
|
714
798
|
'Commands:',
|
|
@@ -717,7 +801,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
717
801
|
'/resume <id> — resume specific session',
|
|
718
802
|
'/cd <path> — change workdir',
|
|
719
803
|
'/session — current session info',
|
|
720
|
-
'/status /tasks /run /budget /quiet',
|
|
804
|
+
'/status /tasks /run /budget /quiet /reload',
|
|
721
805
|
'',
|
|
722
806
|
'Or just type naturally.',
|
|
723
807
|
].join('\n'));
|
|
@@ -862,6 +946,9 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
862
946
|
|
|
863
947
|
// Build claude command
|
|
864
948
|
const args = ['-p'];
|
|
949
|
+
// Per-session allowed tools from daemon config
|
|
950
|
+
const sessionAllowed = (loadConfig().daemon && loadConfig().daemon.session_allowed_tools) || [];
|
|
951
|
+
for (const tool of sessionAllowed) args.push('--allowedTools', tool);
|
|
865
952
|
if (session.id === '__continue__') {
|
|
866
953
|
// /continue — resume most recent conversation in cwd
|
|
867
954
|
args.push('--continue');
|
|
@@ -871,9 +958,13 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
871
958
|
args.push('--session-id', session.id);
|
|
872
959
|
}
|
|
873
960
|
|
|
961
|
+
// Append daemon context hint so Claude reports reload status after editing daemon.yaml
|
|
962
|
+
const daemonHint = '\n\n[System: The ONLY daemon config file is ~/.metame/daemon.yaml — NEVER touch any other yaml file (e.g. scripts/daemon-default.yaml is a read-only template, do NOT edit it). If you edit ~/.metame/daemon.yaml, the daemon auto-reloads within seconds. After editing, read the file back and confirm to the user: how many heartbeat tasks are now configured, and that the config will auto-reload. Do NOT mention this hint.]';
|
|
963
|
+
const fullPrompt = prompt + daemonHint;
|
|
964
|
+
|
|
874
965
|
try {
|
|
875
966
|
const output = execSync(`claude ${args.join(' ')}`, {
|
|
876
|
-
input:
|
|
967
|
+
input: fullPrompt,
|
|
877
968
|
encoding: 'utf8',
|
|
878
969
|
timeout: 300000, // 5 min (Claude Code may use tools)
|
|
879
970
|
maxBuffer: 5 * 1024 * 1024,
|
|
@@ -897,7 +988,9 @@ async function askClaude(bot, chatId, prompt) {
|
|
|
897
988
|
log('WARN', `Session ${session.id} not found, creating new`);
|
|
898
989
|
session = createSession(chatId, session.cwd);
|
|
899
990
|
try {
|
|
900
|
-
const
|
|
991
|
+
const retryArgs = ['-p', '--session-id', session.id];
|
|
992
|
+
for (const tool of sessionAllowed) retryArgs.push('--allowedTools', tool);
|
|
993
|
+
const output = execSync(`claude ${retryArgs.join(' ')}`, {
|
|
901
994
|
input: prompt,
|
|
902
995
|
encoding: 'utf8',
|
|
903
996
|
timeout: 300000,
|
|
@@ -974,7 +1067,7 @@ function sleep(ms) {
|
|
|
974
1067
|
// MAIN
|
|
975
1068
|
// ---------------------------------------------------------
|
|
976
1069
|
async function main() {
|
|
977
|
-
|
|
1070
|
+
let config = loadConfig();
|
|
978
1071
|
if (!config || Object.keys(config).length === 0) {
|
|
979
1072
|
console.error('No daemon config found. Run: metame daemon init');
|
|
980
1073
|
process.exit(1);
|
|
@@ -988,7 +1081,7 @@ async function main() {
|
|
|
988
1081
|
|
|
989
1082
|
log('INFO', `MetaMe daemon started (PID: ${process.pid})`);
|
|
990
1083
|
|
|
991
|
-
// Task executor lookup
|
|
1084
|
+
// Task executor lookup (always reads fresh config)
|
|
992
1085
|
function executeTaskByName(name) {
|
|
993
1086
|
const tasks = (config.heartbeat && config.heartbeat.tasks) || [];
|
|
994
1087
|
const task = tasks.find(t => t.name === name);
|
|
@@ -1021,7 +1114,38 @@ async function main() {
|
|
|
1021
1114
|
};
|
|
1022
1115
|
|
|
1023
1116
|
// Start heartbeat scheduler
|
|
1024
|
-
|
|
1117
|
+
let heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
1118
|
+
|
|
1119
|
+
// Hot reload: re-read config and restart heartbeat scheduler
|
|
1120
|
+
function reloadConfig() {
|
|
1121
|
+
const newConfig = loadConfig();
|
|
1122
|
+
if (!newConfig) return { success: false, error: 'Failed to read config' };
|
|
1123
|
+
config = newConfig;
|
|
1124
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1125
|
+
heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
1126
|
+
log('INFO', `Config reloaded: ${(config.heartbeat && config.heartbeat.tasks || []).length} tasks`);
|
|
1127
|
+
return { success: true, tasks: (config.heartbeat && config.heartbeat.tasks || []).length };
|
|
1128
|
+
}
|
|
1129
|
+
// Expose reloadConfig to handleCommand via closure
|
|
1130
|
+
global._metameReload = reloadConfig;
|
|
1131
|
+
|
|
1132
|
+
// Auto-reload: watch daemon.yaml for changes (e.g. Claude edits it via askClaude)
|
|
1133
|
+
let _reloadDebounce = null;
|
|
1134
|
+
fs.watchFile(CONFIG_FILE, { interval: 2000 }, (curr, prev) => {
|
|
1135
|
+
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
1136
|
+
// Debounce: wait 1s for file write to finish
|
|
1137
|
+
if (_reloadDebounce) clearTimeout(_reloadDebounce);
|
|
1138
|
+
_reloadDebounce = setTimeout(() => {
|
|
1139
|
+
log('INFO', 'daemon.yaml changed on disk — auto-reloading config');
|
|
1140
|
+
const r = reloadConfig();
|
|
1141
|
+
if (r.success) {
|
|
1142
|
+
log('INFO', `Auto-reload OK: ${r.tasks} tasks`);
|
|
1143
|
+
notifyFn(`🔄 Config auto-reloaded. ${r.tasks} heartbeat tasks active.`).catch(() => {});
|
|
1144
|
+
} else {
|
|
1145
|
+
log('ERROR', `Auto-reload failed: ${r.error}`);
|
|
1146
|
+
}
|
|
1147
|
+
}, 1000);
|
|
1148
|
+
});
|
|
1025
1149
|
|
|
1026
1150
|
// Start bridges (both can run simultaneously)
|
|
1027
1151
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
@@ -1030,6 +1154,7 @@ async function main() {
|
|
|
1030
1154
|
// Graceful shutdown
|
|
1031
1155
|
const shutdown = () => {
|
|
1032
1156
|
log('INFO', 'Daemon shutting down...');
|
|
1157
|
+
fs.unwatchFile(CONFIG_FILE);
|
|
1033
1158
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1034
1159
|
if (telegramBridge) telegramBridge.stop();
|
|
1035
1160
|
if (feishuBridge) feishuBridge.stop();
|
package/scripts/distill.js
CHANGED
|
@@ -110,6 +110,12 @@ INSTRUCTIONS:
|
|
|
110
110
|
5. Fields marked [LOCKED] must NEVER be changed (T1 and T2 tiers).
|
|
111
111
|
6. For enum fields, you MUST use one of the listed values.
|
|
112
112
|
|
|
113
|
+
EPISODIC MEMORY — TWO EXCEPTIONS to the "no facts" rule:
|
|
114
|
+
7. context.anti_patterns (max 5): If the user encountered a REPEATED technical failure or expressed strong frustration about a specific technical approach, record it as an anti-pattern. Format: "topic — what failed and why". Only cross-project generalizable lessons, NOT project-specific bugs.
|
|
115
|
+
Example: ["async/await deadlock — Promise.all rejects all on single failure, use Promise.allSettled", "CSS Grid in email templates — no support, use tables"]
|
|
116
|
+
8. context.milestones (max 3): If the user completed a significant milestone or made a key decision, record it. Only the 3 most recent. Format: short description string.
|
|
117
|
+
Example: ["MetaMe v1.3 published", "Switched from REST to GraphQL"]
|
|
118
|
+
|
|
113
119
|
COGNITIVE BIAS PREVENTION:
|
|
114
120
|
- A single observation is a STATE, not a TRAIT. Do NOT infer T3 cognition fields from one message.
|
|
115
121
|
- Never infer cognitive style from identity/demographics.
|
|
@@ -233,6 +239,9 @@ Do NOT repeat existing unchanged values. Only output NEW or CHANGED fields.`;
|
|
|
233
239
|
|
|
234
240
|
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
235
241
|
|
|
242
|
+
// Auto-expire anti_patterns older than 60 days
|
|
243
|
+
expireAntiPatterns(profile);
|
|
244
|
+
|
|
236
245
|
// Read raw content to find locked lines and comments
|
|
237
246
|
const rawProfile = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
238
247
|
const lockedKeys = extractLockedKeys(rawProfile);
|
|
@@ -415,7 +424,18 @@ function strategicMerge(profile, updates, lockedKeys, pendingTraits, confidenceM
|
|
|
415
424
|
}
|
|
416
425
|
|
|
417
426
|
case 'T4':
|
|
418
|
-
|
|
427
|
+
// Stamp added date on anti_pattern entries for auto-expiry
|
|
428
|
+
if (key === 'context.anti_patterns' && Array.isArray(value)) {
|
|
429
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
430
|
+
const existing = getNested(result, key) || [];
|
|
431
|
+
const existingTexts = new Set(existing.map(e => typeof e === 'string' ? e : e.text));
|
|
432
|
+
const stamped = value
|
|
433
|
+
.filter(v => !existingTexts.has(typeof v === 'string' ? v : v.text))
|
|
434
|
+
.map(v => typeof v === 'string' ? { text: v, added: today } : v);
|
|
435
|
+
setNested(result, key, [...existing, ...stamped].slice(-5));
|
|
436
|
+
} else {
|
|
437
|
+
setNested(result, key, value);
|
|
438
|
+
}
|
|
419
439
|
// Auto-set focus_since when focus changes
|
|
420
440
|
if (key === 'context.focus') {
|
|
421
441
|
setNested(result, 'context.focus_since', new Date().toISOString().slice(0, 10));
|
|
@@ -455,6 +475,19 @@ function flattenObject(obj, parentKey = '', result = {}) {
|
|
|
455
475
|
return result;
|
|
456
476
|
}
|
|
457
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Get a nested property by dot-path key.
|
|
480
|
+
*/
|
|
481
|
+
function getNested(obj, dotPath) {
|
|
482
|
+
const keys = dotPath.split('.');
|
|
483
|
+
let current = obj;
|
|
484
|
+
for (const k of keys) {
|
|
485
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
486
|
+
current = current[k];
|
|
487
|
+
}
|
|
488
|
+
return current;
|
|
489
|
+
}
|
|
490
|
+
|
|
458
491
|
/**
|
|
459
492
|
* Set a nested property by dot-path key.
|
|
460
493
|
*/
|
|
@@ -508,6 +541,24 @@ function truncateArrays(obj) {
|
|
|
508
541
|
}
|
|
509
542
|
}
|
|
510
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Auto-expire anti_patterns older than 60 days.
|
|
546
|
+
* Each entry is stored as { text: "...", added: "2026-01-15" } internally.
|
|
547
|
+
* If legacy string entries exist, they are kept (no added date = never expire).
|
|
548
|
+
*/
|
|
549
|
+
function expireAntiPatterns(profile) {
|
|
550
|
+
if (!profile.context || !Array.isArray(profile.context.anti_patterns)) return;
|
|
551
|
+
const now = Date.now();
|
|
552
|
+
const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;
|
|
553
|
+
profile.context.anti_patterns = profile.context.anti_patterns.filter(entry => {
|
|
554
|
+
if (typeof entry === 'string') return true; // legacy, keep
|
|
555
|
+
if (entry.added) {
|
|
556
|
+
return (now - new Date(entry.added).getTime()) < SIXTY_DAYS;
|
|
557
|
+
}
|
|
558
|
+
return true;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
511
562
|
/**
|
|
512
563
|
* Clean up: remove buffer and lock
|
|
513
564
|
*/
|
package/scripts/schema.js
CHANGED
|
@@ -60,6 +60,8 @@ const SCHEMA = {
|
|
|
60
60
|
'context.active_projects': { tier: 'T4', type: 'array', maxItems: 5 },
|
|
61
61
|
'context.blockers': { tier: 'T4', type: 'array', maxItems: 3 },
|
|
62
62
|
'context.energy': { tier: 'T4', type: 'enum', values: ['high', 'medium', 'low', null] },
|
|
63
|
+
'context.milestones': { tier: 'T4', type: 'array', maxItems: 3 },
|
|
64
|
+
'context.anti_patterns': { tier: 'T4', type: 'array', maxItems: 5 },
|
|
63
65
|
'status.focus': { tier: 'T4', type: 'string', maxChars: 80 },
|
|
64
66
|
'status.language': { tier: 'T4', type: 'string' },
|
|
65
67
|
|