metame-cli 1.4.17 → 1.4.18
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 +9 -6
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +350 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +57 -10
- package/scripts/daemon-command-router.js +241 -3
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +213 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +371 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +158 -19
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
|
@@ -13,6 +13,14 @@ const path = require('path');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
|
|
15
15
|
const BUFFER_FILE = path.join(os.homedir(), '.metame', 'raw_signals.jsonl');
|
|
16
|
+
const OVERFLOW_FILE = path.join(os.homedir(), '.metame', 'raw_signals.overflow.jsonl');
|
|
17
|
+
const LOCK_FILE = path.join(os.homedir(), '.metame', 'raw_signals.lock');
|
|
18
|
+
const LOCK_RETRY_MAX = 80;
|
|
19
|
+
const LOCK_RETRY_MS = 8;
|
|
20
|
+
const LOCK_STALE_MS = 30 * 1000;
|
|
21
|
+
const MAX_BUFFER_LINES = 300;
|
|
22
|
+
const MAX_CAPTURE_CHARS = 1600;
|
|
23
|
+
const ABSOLUTE_MAX_CAPTURE_CHARS = 6000;
|
|
16
24
|
|
|
17
25
|
// === CONFIDENCE PATTERNS ===
|
|
18
26
|
|
|
@@ -33,6 +41,90 @@ const CORRECTION_EN = /(no,? I meant|that's not what I|you misunderstood|wrong.+
|
|
|
33
41
|
const META_ZH = /我(发现|意识到|觉得|反思|总结|复盘)|想错了|换个(思路|方向|方案)|回头(想想|看看)|之前的(方案|思路|方向).*(不行|不对|有问题)|我的(问题|毛病|习惯)是|下次(应该|要|得)/;
|
|
34
42
|
const META_EN = /(I realize|looking back|on reflection|my (mistake|problem|habit) is|let me rethink|wrong approach|next time I should)/i;
|
|
35
43
|
|
|
44
|
+
// Internal/system prompts must never enter cognition signal buffer.
|
|
45
|
+
const INTERNAL_PROMPT_PATTERNS = [
|
|
46
|
+
/You are a MetaMe cognitive profile distiller/i,
|
|
47
|
+
/You are a metacognition pattern detector/i,
|
|
48
|
+
/你是精准的知识提取引擎/,
|
|
49
|
+
/RECALLED LONG-TERM FACTS \(context only/i,
|
|
50
|
+
/\[System hints - DO NOT mention these to user:/i,
|
|
51
|
+
/\[Mac automation policy - do NOT expose this block:/i,
|
|
52
|
+
/MANDATORY FIRST ACTION: The user has not been calibrated yet/i,
|
|
53
|
+
/<\!--\s*FACTS:START\s*-->/i,
|
|
54
|
+
/<\!--\s*MEMORY:START\s*-->/i,
|
|
55
|
+
/\[Task notification\]/i,
|
|
56
|
+
/<task-notification\b/i,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function sanitizeMetaMePrompt(text) {
|
|
60
|
+
let prompt = String(text || '');
|
|
61
|
+
if (!prompt) return '';
|
|
62
|
+
|
|
63
|
+
// Remove daemon-injected RAG blocks
|
|
64
|
+
prompt = prompt.replace(/<!--\s*FACTS:START\s*-->[\s\S]*?<!--\s*FACTS:END\s*-->/gi, ' ');
|
|
65
|
+
prompt = prompt.replace(/<!--\s*MEMORY:START\s*-->[\s\S]*?<!--\s*MEMORY:END\s*-->/gi, ' ');
|
|
66
|
+
|
|
67
|
+
// Remove daemon/system internal hint blocks
|
|
68
|
+
prompt = prompt.replace(/\[System hints - DO NOT mention these to user:[\s\S]*?\]/gi, ' ');
|
|
69
|
+
prompt = prompt.replace(/\[Mac automation policy - do NOT expose this block:[\s\S]*?\]/gi, ' ');
|
|
70
|
+
prompt = prompt.replace(/\[Task notification\][\s\S]*?(?=\n{2,}|$)/gi, ' ');
|
|
71
|
+
prompt = prompt.replace(/<task-notification\b[\s\S]*?<\/task-notification>/gi, ' ');
|
|
72
|
+
prompt = prompt.replace(/<task-notification\b[\s\S]*$/gi, ' ');
|
|
73
|
+
|
|
74
|
+
return prompt.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isInternalPrompt(text) {
|
|
78
|
+
const prompt = String(text || '');
|
|
79
|
+
if (!prompt) return false;
|
|
80
|
+
return INTERNAL_PROMPT_PATTERNS.some((re) => re.test(prompt));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sleep(ms) {
|
|
84
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withBufferLock(fn) {
|
|
88
|
+
const dir = path.dirname(BUFFER_FILE);
|
|
89
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
let acquired = false;
|
|
92
|
+
for (let i = 0; i < LOCK_RETRY_MAX; i++) {
|
|
93
|
+
try {
|
|
94
|
+
const fd = fs.openSync(LOCK_FILE, 'wx');
|
|
95
|
+
fs.writeSync(fd, process.pid.toString());
|
|
96
|
+
fs.closeSync(fd);
|
|
97
|
+
acquired = true;
|
|
98
|
+
break;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (e.code !== 'EEXIST') throw e;
|
|
101
|
+
try {
|
|
102
|
+
const age = Date.now() - fs.statSync(LOCK_FILE).mtimeMs;
|
|
103
|
+
if (age > LOCK_STALE_MS) {
|
|
104
|
+
fs.unlinkSync(LOCK_FILE);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
} catch { /* lock released by another process */ }
|
|
108
|
+
sleep(LOCK_RETRY_MS);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!acquired) return false;
|
|
113
|
+
try {
|
|
114
|
+
fn();
|
|
115
|
+
return true;
|
|
116
|
+
} finally {
|
|
117
|
+
try { fs.unlinkSync(LOCK_FILE); } catch { /* non-fatal */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeBufferAtomically(lines) {
|
|
122
|
+
const tmp = BUFFER_FILE + `.tmp.${process.pid}`;
|
|
123
|
+
const content = lines.length > 0 ? (lines.join('\n') + '\n') : '';
|
|
124
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
125
|
+
fs.renameSync(tmp, BUFFER_FILE);
|
|
126
|
+
}
|
|
127
|
+
|
|
36
128
|
// Read JSON from stdin
|
|
37
129
|
let input = '';
|
|
38
130
|
process.stdin.setEncoding('utf8');
|
|
@@ -40,7 +132,23 @@ process.stdin.on('data', (chunk) => { input += chunk; });
|
|
|
40
132
|
process.stdin.on('end', () => {
|
|
41
133
|
try {
|
|
42
134
|
const data = JSON.parse(input);
|
|
43
|
-
|
|
135
|
+
let prompt = (data.prompt || '').trim();
|
|
136
|
+
|
|
137
|
+
// Internal Claude subprocesses (distill/memory extract/skill evolution) set this flag.
|
|
138
|
+
if (process.env.METAME_INTERNAL_PROMPT === '1') {
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Strip daemon/system injected wrappers first; keep user payload if present.
|
|
143
|
+
prompt = sanitizeMetaMePrompt(prompt);
|
|
144
|
+
if (!prompt) {
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Belt-and-suspenders: filter known internal prompt templates.
|
|
149
|
+
if (isInternalPrompt(prompt)) {
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
44
152
|
|
|
45
153
|
// === LAYER 0: Metacognitive bypass — always capture self-reflection ===
|
|
46
154
|
const isMeta = META_ZH.test(prompt) || META_EN.test(prompt);
|
|
@@ -55,6 +163,14 @@ process.stdin.on('end', () => {
|
|
|
55
163
|
process.exit(0);
|
|
56
164
|
}
|
|
57
165
|
|
|
166
|
+
// Hard cap to prevent giant prompt pastes from poisoning distill budget.
|
|
167
|
+
if (prompt.length > ABSOLUTE_MAX_CAPTURE_CHARS) {
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
if (prompt.length > MAX_CAPTURE_CHARS) {
|
|
171
|
+
prompt = prompt.slice(0, MAX_CAPTURE_CHARS);
|
|
172
|
+
}
|
|
173
|
+
|
|
58
174
|
// Skip messages that are purely code or file paths
|
|
59
175
|
if (!isMeta && /^(```|\/[\w/]+\.\w+$)/.test(prompt)) {
|
|
60
176
|
process.exit(0);
|
|
@@ -113,18 +229,62 @@ process.stdin.on('end', () => {
|
|
|
113
229
|
cwd: data.cwd || null
|
|
114
230
|
};
|
|
115
231
|
|
|
116
|
-
// Append
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.split('\n').filter(l => l.trim());
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
232
|
+
// Append/update with process-level lock to avoid concurrent read-modify-write loss.
|
|
233
|
+
const locked = withBufferLock(() => {
|
|
234
|
+
let existingLines = [];
|
|
235
|
+
try {
|
|
236
|
+
existingLines = fs.readFileSync(BUFFER_FILE, 'utf8').split('\n').filter(l => l.trim());
|
|
237
|
+
} catch {
|
|
238
|
+
// File doesn't exist yet, that's fine
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Drain overflow written during prior lock-contention periods.
|
|
242
|
+
// unlink AFTER writeBufferAtomically succeeds — crash-safe ordering.
|
|
243
|
+
let overflowDrained = false;
|
|
244
|
+
try {
|
|
245
|
+
const overflowLines = fs.readFileSync(OVERFLOW_FILE, 'utf8').split('\n').filter(Boolean);
|
|
246
|
+
if (overflowLines.length > 0) {
|
|
247
|
+
existingLines = existingLines.concat(overflowLines);
|
|
248
|
+
overflowDrained = true;
|
|
249
|
+
}
|
|
250
|
+
} catch { /* no overflow file — normal case */ }
|
|
124
251
|
|
|
125
|
-
|
|
252
|
+
// Opportunistic hygiene: remove old internal/system lines if any slipped in historically.
|
|
253
|
+
existingLines = existingLines.filter((line) => {
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(line);
|
|
256
|
+
const p = String(parsed && parsed.prompt ? parsed.prompt : '');
|
|
257
|
+
if (!p) return false;
|
|
258
|
+
if (p.length > ABSOLUTE_MAX_CAPTURE_CHARS) return false;
|
|
259
|
+
if (isInternalPrompt(p)) return false;
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
126
265
|
|
|
127
|
-
|
|
266
|
+
existingLines.push(JSON.stringify(entry));
|
|
267
|
+
if (existingLines.length > MAX_BUFFER_LINES) {
|
|
268
|
+
existingLines = existingLines.slice(-MAX_BUFFER_LINES);
|
|
269
|
+
}
|
|
270
|
+
writeBufferAtomically(existingLines);
|
|
271
|
+
// Unlink overflow only after main file is safely written.
|
|
272
|
+
if (overflowDrained) {
|
|
273
|
+
try { fs.unlinkSync(OVERFLOW_FILE); } catch { /* already gone */ }
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!locked) {
|
|
278
|
+
// Last-resort fallback: write to overflow side-file so the next lock-holder
|
|
279
|
+
// can drain, clean, and cap it — never bypassing buffer rules.
|
|
280
|
+
// Guard against unbounded growth: drop entry if overflow already at cap.
|
|
281
|
+
fs.mkdirSync(path.dirname(OVERFLOW_FILE), { recursive: true });
|
|
282
|
+
try {
|
|
283
|
+
const ofLines = fs.readFileSync(OVERFLOW_FILE, 'utf8').split('\n').filter(Boolean);
|
|
284
|
+
if (ofLines.length >= MAX_BUFFER_LINES) return; // shed load
|
|
285
|
+
} catch { /* overflow file doesn't exist yet */ }
|
|
286
|
+
fs.appendFileSync(OVERFLOW_FILE, JSON.stringify(entry) + '\n', 'utf8');
|
|
287
|
+
}
|
|
128
288
|
|
|
129
289
|
} catch {
|
|
130
290
|
// Silently ignore parse errors — never block the user's workflow
|
|
@@ -35,6 +35,8 @@ const os = require('os');
|
|
|
35
35
|
const HOME = os.homedir();
|
|
36
36
|
const METAME_DIR = path.join(HOME, '.metame');
|
|
37
37
|
const SKILL_SIGNAL_FILE = path.join(METAME_DIR, 'skill_signals.jsonl');
|
|
38
|
+
const SKILL_SIGNAL_OVERFLOW_FILE = path.join(METAME_DIR, 'skill_signals.overflow.jsonl');
|
|
39
|
+
const SKILL_SIGNAL_LOCK_FILE = path.join(METAME_DIR, 'skill_signals.lock');
|
|
38
40
|
const EVOLUTION_QUEUE_FILE = path.join(METAME_DIR, 'evolution_queue.yaml');
|
|
39
41
|
const EVOLUTION_POLICY_FILE = path.join(METAME_DIR, 'evolution_policy.yaml');
|
|
40
42
|
const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
@@ -113,6 +115,54 @@ Respond with ONLY a JSON code block:
|
|
|
113
115
|
If no actionable insights, respond with exactly: NO_EVOLUTION`,
|
|
114
116
|
};
|
|
115
117
|
|
|
118
|
+
function clampInt(value, fallback, min, max) {
|
|
119
|
+
const n = Number(value);
|
|
120
|
+
if (!Number.isFinite(n)) return fallback;
|
|
121
|
+
const i = Math.floor(n);
|
|
122
|
+
return Math.max(min, Math.min(max, i));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sanitizePatternList(value, fallback) {
|
|
126
|
+
const list = Array.isArray(value) ? value : fallback;
|
|
127
|
+
const clean = [];
|
|
128
|
+
for (const v of list) {
|
|
129
|
+
if (typeof v !== 'string') continue;
|
|
130
|
+
const p = v.trim();
|
|
131
|
+
if (!p || p.length > 300) continue;
|
|
132
|
+
try {
|
|
133
|
+
// Validate regex compileability once so hot path won't crash at runtime.
|
|
134
|
+
// eslint-disable-next-line no-new
|
|
135
|
+
new RegExp(p, 'i');
|
|
136
|
+
clean.push(p);
|
|
137
|
+
} catch { /* invalid pattern */ }
|
|
138
|
+
}
|
|
139
|
+
return clean.length > 0 ? clean : fallback.slice();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sanitizePolicy(input) {
|
|
143
|
+
const merged = { ...DEFAULT_POLICY, ...(input && typeof input === 'object' ? input : {}) };
|
|
144
|
+
const policy = {
|
|
145
|
+
...merged,
|
|
146
|
+
version: clampInt(merged.version, DEFAULT_POLICY.version, 1, 1000000),
|
|
147
|
+
hot_failure_threshold: clampInt(merged.hot_failure_threshold, DEFAULT_POLICY.hot_failure_threshold, 1, 50),
|
|
148
|
+
hot_failure_window_minutes: clampInt(merged.hot_failure_window_minutes, DEFAULT_POLICY.hot_failure_window_minutes, 1, 24 * 60),
|
|
149
|
+
min_signals_for_distill: clampInt(merged.min_signals_for_distill, DEFAULT_POLICY.min_signals_for_distill, 1, 5000),
|
|
150
|
+
max_signals_buffer: clampInt(merged.max_signals_buffer, DEFAULT_POLICY.max_signals_buffer, 20, 20000),
|
|
151
|
+
min_evidence_for_update: clampInt(merged.min_evidence_for_update, DEFAULT_POLICY.min_evidence_for_update, 1, 20),
|
|
152
|
+
min_evidence_for_gap: clampInt(merged.min_evidence_for_gap, DEFAULT_POLICY.min_evidence_for_gap, 1, 20),
|
|
153
|
+
max_updates_per_analysis: clampInt(merged.max_updates_per_analysis, DEFAULT_POLICY.max_updates_per_analysis, 1, 20),
|
|
154
|
+
max_gaps_per_analysis: clampInt(merged.max_gaps_per_analysis, DEFAULT_POLICY.max_gaps_per_analysis, 1, 20),
|
|
155
|
+
self_eval_interval: clampInt(merged.self_eval_interval, DEFAULT_POLICY.self_eval_interval, 1, 1000),
|
|
156
|
+
cold_path_run_count: clampInt(merged.cold_path_run_count, DEFAULT_POLICY.cold_path_run_count, 0, 1000000),
|
|
157
|
+
complaint_patterns: sanitizePatternList(merged.complaint_patterns, DEFAULT_POLICY.complaint_patterns),
|
|
158
|
+
missing_skill_patterns: sanitizePatternList(merged.missing_skill_patterns, DEFAULT_POLICY.missing_skill_patterns),
|
|
159
|
+
prompt_template: (typeof merged.prompt_template === 'string' && merged.prompt_template.trim())
|
|
160
|
+
? merged.prompt_template
|
|
161
|
+
: DEFAULT_POLICY.prompt_template,
|
|
162
|
+
};
|
|
163
|
+
return policy;
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
function loadPolicy() {
|
|
117
167
|
let yaml;
|
|
118
168
|
try { yaml = require('js-yaml'); } catch { return { ...DEFAULT_POLICY }; }
|
|
@@ -125,8 +175,7 @@ function loadPolicy() {
|
|
|
125
175
|
}
|
|
126
176
|
const content = fs.readFileSync(EVOLUTION_POLICY_FILE, 'utf8');
|
|
127
177
|
const loaded = yaml.load(content) || {};
|
|
128
|
-
|
|
129
|
-
return { ...DEFAULT_POLICY, ...loaded };
|
|
178
|
+
return sanitizePolicy(loaded);
|
|
130
179
|
} catch {
|
|
131
180
|
return { ...DEFAULT_POLICY };
|
|
132
181
|
}
|
|
@@ -135,7 +184,7 @@ function loadPolicy() {
|
|
|
135
184
|
function savePolicy(yaml, policy) {
|
|
136
185
|
try {
|
|
137
186
|
if (!fs.existsSync(METAME_DIR)) fs.mkdirSync(METAME_DIR, { mode: 0o700, recursive: true });
|
|
138
|
-
fs.writeFileSync(EVOLUTION_POLICY_FILE, yaml.dump(policy, { lineWidth: 120 }), 'utf8');
|
|
187
|
+
fs.writeFileSync(EVOLUTION_POLICY_FILE, yaml.dump(sanitizePolicy(policy), { lineWidth: 120 }), 'utf8');
|
|
139
188
|
} catch {}
|
|
140
189
|
}
|
|
141
190
|
|
|
@@ -197,20 +246,92 @@ function extractSkillSignal(prompt, output, error, files, cwd, toolUsageLog) {
|
|
|
197
246
|
/**
|
|
198
247
|
* Append a skill signal to the JSONL buffer.
|
|
199
248
|
*/
|
|
249
|
+
function withSkillSignalLock(fn) {
|
|
250
|
+
if (!fs.existsSync(METAME_DIR)) fs.mkdirSync(METAME_DIR, { mode: 0o700, recursive: true });
|
|
251
|
+
const maxRetry = 80;
|
|
252
|
+
const retryMs = 8;
|
|
253
|
+
const staleMs = 30 * 1000;
|
|
254
|
+
|
|
255
|
+
let acquired = false;
|
|
256
|
+
for (let i = 0; i < maxRetry; i++) {
|
|
257
|
+
try {
|
|
258
|
+
const fd = fs.openSync(SKILL_SIGNAL_LOCK_FILE, 'wx');
|
|
259
|
+
fs.writeSync(fd, String(process.pid));
|
|
260
|
+
fs.closeSync(fd);
|
|
261
|
+
acquired = true;
|
|
262
|
+
break;
|
|
263
|
+
} catch (e) {
|
|
264
|
+
if (e.code !== 'EEXIST') throw e;
|
|
265
|
+
try {
|
|
266
|
+
const age = Date.now() - fs.statSync(SKILL_SIGNAL_LOCK_FILE).mtimeMs;
|
|
267
|
+
if (age > staleMs) {
|
|
268
|
+
fs.unlinkSync(SKILL_SIGNAL_LOCK_FILE);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
} catch { /* lock released elsewhere */ }
|
|
272
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryMs);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!acquired) return false;
|
|
277
|
+
try {
|
|
278
|
+
fn();
|
|
279
|
+
return true;
|
|
280
|
+
} finally {
|
|
281
|
+
try { fs.unlinkSync(SKILL_SIGNAL_LOCK_FILE); } catch {}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function writeSkillSignalLines(lines) {
|
|
286
|
+
const tmp = SKILL_SIGNAL_FILE + `.tmp.${process.pid}`;
|
|
287
|
+
const content = Array.isArray(lines) && lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
288
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
289
|
+
fs.renameSync(tmp, SKILL_SIGNAL_FILE);
|
|
290
|
+
}
|
|
291
|
+
|
|
200
292
|
function appendSkillSignal(signal) {
|
|
201
293
|
if (!signal) return;
|
|
202
294
|
try {
|
|
203
|
-
|
|
295
|
+
const payload = JSON.stringify(signal);
|
|
296
|
+
const policy = loadPolicy();
|
|
204
297
|
|
|
205
|
-
|
|
206
|
-
|
|
298
|
+
const locked = withSkillSignalLock(() => {
|
|
299
|
+
let lines = [];
|
|
300
|
+
try {
|
|
301
|
+
lines = fs.readFileSync(SKILL_SIGNAL_FILE, 'utf8').split('\n').filter(Boolean);
|
|
302
|
+
} catch { /* first write */ }
|
|
303
|
+
|
|
304
|
+
// Drain overflow written during prior lock-contention periods.
|
|
305
|
+
// unlink AFTER writeSkillSignalLines succeeds — crash-safe ordering.
|
|
306
|
+
let overflowDrained = false;
|
|
307
|
+
try {
|
|
308
|
+
const overflowLines = fs.readFileSync(SKILL_SIGNAL_OVERFLOW_FILE, 'utf8').split('\n').filter(Boolean);
|
|
309
|
+
if (overflowLines.length > 0) {
|
|
310
|
+
lines = lines.concat(overflowLines);
|
|
311
|
+
overflowDrained = true;
|
|
312
|
+
}
|
|
313
|
+
} catch { /* no overflow file — normal case */ }
|
|
314
|
+
|
|
315
|
+
lines.push(payload);
|
|
316
|
+
if (lines.length > policy.max_signals_buffer) {
|
|
317
|
+
lines = lines.slice(-policy.max_signals_buffer);
|
|
318
|
+
}
|
|
319
|
+
writeSkillSignalLines(lines);
|
|
320
|
+
// Unlink overflow only after main file is safely written.
|
|
321
|
+
if (overflowDrained) {
|
|
322
|
+
try { fs.unlinkSync(SKILL_SIGNAL_OVERFLOW_FILE); } catch { /* already gone */ }
|
|
323
|
+
}
|
|
324
|
+
});
|
|
207
325
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
326
|
+
if (!locked) {
|
|
327
|
+
// Last-resort fallback: write to overflow side-file so the next lock-holder
|
|
328
|
+
// can drain and apply max_signals_buffer cap — never bypassing buffer rules.
|
|
329
|
+
// Guard against unbounded growth: drop entry if overflow already at cap.
|
|
330
|
+
try {
|
|
331
|
+
const ofLines = fs.readFileSync(SKILL_SIGNAL_OVERFLOW_FILE, 'utf8').split('\n').filter(Boolean);
|
|
332
|
+
if (ofLines.length >= policy.max_signals_buffer) return;
|
|
333
|
+
} catch { /* overflow file doesn't exist yet */ }
|
|
334
|
+
fs.appendFileSync(SKILL_SIGNAL_OVERFLOW_FILE, payload + '\n', 'utf8');
|
|
214
335
|
}
|
|
215
336
|
} catch {
|
|
216
337
|
// Non-fatal
|
|
@@ -263,7 +384,12 @@ function checkHotEvolution(signal) {
|
|
|
263
384
|
}
|
|
264
385
|
|
|
265
386
|
// Rule 2: User complaints about a skill
|
|
266
|
-
|
|
387
|
+
let complaintRe;
|
|
388
|
+
try {
|
|
389
|
+
complaintRe = new RegExp(policy.complaint_patterns.join('|'), 'i');
|
|
390
|
+
} catch {
|
|
391
|
+
complaintRe = new RegExp(DEFAULT_POLICY.complaint_patterns.join('|'), 'i');
|
|
392
|
+
}
|
|
267
393
|
if (signal.prompt && complaintRe.test(signal.prompt) && signal.skills_invoked.length > 0) {
|
|
268
394
|
for (const sk of signal.skills_invoked) {
|
|
269
395
|
addToQueue(queue, {
|
|
@@ -276,7 +402,12 @@ function checkHotEvolution(signal) {
|
|
|
276
402
|
}
|
|
277
403
|
|
|
278
404
|
// Rule 3: Missing skill detection
|
|
279
|
-
|
|
405
|
+
let missingRe;
|
|
406
|
+
try {
|
|
407
|
+
missingRe = new RegExp(policy.missing_skill_patterns.join('|'), 'i');
|
|
408
|
+
} catch {
|
|
409
|
+
missingRe = new RegExp(DEFAULT_POLICY.missing_skill_patterns.join('|'), 'i');
|
|
410
|
+
}
|
|
280
411
|
const missText = [(signal.error || ''), (signal.prompt || ''), (signal.output_excerpt || '')].join('\n');
|
|
281
412
|
const hasMissingPattern = missingRe.test(missText);
|
|
282
413
|
const hasExplicitSkillMiss = signal.has_tool_failure &&
|
|
@@ -414,7 +545,6 @@ async function distillSkills() {
|
|
|
414
545
|
// Get installed skills list
|
|
415
546
|
const installedSkills = listInstalledSkills();
|
|
416
547
|
if (installedSkills.length === 0) {
|
|
417
|
-
clearSignals();
|
|
418
548
|
return null;
|
|
419
549
|
}
|
|
420
550
|
|
|
@@ -466,7 +596,6 @@ async function distillSkills() {
|
|
|
466
596
|
|
|
467
597
|
const jsonMatch = result.match(/```json\s*([\s\S]*?)```/);
|
|
468
598
|
if (!jsonMatch) {
|
|
469
|
-
clearSignals();
|
|
470
599
|
return null;
|
|
471
600
|
}
|
|
472
601
|
|
|
@@ -624,8 +753,15 @@ RULES:
|
|
|
624
753
|
const patchData = yaml.load(yamlMatch[1]);
|
|
625
754
|
if (!patchData || typeof patchData !== 'object') return;
|
|
626
755
|
|
|
627
|
-
// Apply patch
|
|
628
|
-
const newPolicy = { ...policy, ...patchData };
|
|
756
|
+
// Apply patch with schema/type sanitization.
|
|
757
|
+
const newPolicy = sanitizePolicy({ ...policy, ...patchData });
|
|
758
|
+
if (newPolicy.version <= policy.version) {
|
|
759
|
+
newPolicy.version = policy.version + 1;
|
|
760
|
+
}
|
|
761
|
+
if (JSON.stringify(newPolicy) === JSON.stringify(policy)) {
|
|
762
|
+
console.log('🧬 Policy self-eval: patch produced no effective change.');
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
629
765
|
savePolicy(yaml, newPolicy);
|
|
630
766
|
console.log(`🧬 Policy self-evolved: v${policy.version} → v${newPolicy.version}`);
|
|
631
767
|
|
|
@@ -896,7 +1032,10 @@ function readRecentSignals() {
|
|
|
896
1032
|
}
|
|
897
1033
|
|
|
898
1034
|
function clearSignals() {
|
|
899
|
-
try {
|
|
1035
|
+
try {
|
|
1036
|
+
const locked = withSkillSignalLock(() => writeSkillSignalLines([]));
|
|
1037
|
+
if (!locked) fs.writeFileSync(SKILL_SIGNAL_FILE, '', 'utf8');
|
|
1038
|
+
} catch {}
|
|
900
1039
|
}
|
|
901
1040
|
|
|
902
1041
|
function listInstalledSkills() {
|