metame-cli 1.4.15 → 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 +441 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +71 -22
- package/scripts/daemon-command-router.js +242 -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 +216 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +374 -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 +288 -38
- package/scripts/skill-evolution.test.js +107 -0
- 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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Writes to evolution_queue.yaml for immediate action
|
|
12
12
|
*
|
|
13
13
|
* COLD PATH (batched, Haiku-powered):
|
|
14
|
-
* -
|
|
14
|
+
* - Runs as standalone heartbeat script task (skill-evolve, default 6h)
|
|
15
15
|
* - Haiku analyzes accumulated skill signals for nuanced insights
|
|
16
16
|
* - Merges evolution data into skill's evolution.json + SKILL.md
|
|
17
17
|
*
|
|
@@ -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
|
|
|
@@ -174,7 +223,8 @@ function extractSkillSignal(prompt, output, error, files, cwd, toolUsageLog) {
|
|
|
174
223
|
|
|
175
224
|
const hasSkills = skills.length > 0;
|
|
176
225
|
const hasError = !!error;
|
|
177
|
-
const
|
|
226
|
+
const outputText = typeof output === 'string' ? output : '';
|
|
227
|
+
const hasToolFailure = /(?:failed|error|not found|not available|skill.{0,20}(?:missing|absent|not.{0,10}install))/i.test(outputText);
|
|
178
228
|
|
|
179
229
|
// Skip if no skill involvement and no failure
|
|
180
230
|
if (!hasSkills && !hasError && !hasToolFailure) return null;
|
|
@@ -185,30 +235,103 @@ function extractSkillSignal(prompt, output, error, files, cwd, toolUsageLog) {
|
|
|
185
235
|
skills_invoked: skills,
|
|
186
236
|
tools_used: tools.slice(0, 20), // cap for storage
|
|
187
237
|
error: error ? error.substring(0, 500) : null,
|
|
238
|
+
output_excerpt: outputText.substring(0, 500),
|
|
188
239
|
has_tool_failure: !!hasToolFailure,
|
|
189
240
|
files_modified: (files || []).slice(0, 10),
|
|
190
241
|
cwd: cwd || null,
|
|
191
|
-
outcome:
|
|
242
|
+
outcome: (hasError || hasToolFailure) ? 'error' : (outputText ? 'success' : 'empty'),
|
|
192
243
|
};
|
|
193
244
|
}
|
|
194
245
|
|
|
195
246
|
/**
|
|
196
247
|
* Append a skill signal to the JSONL buffer.
|
|
197
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
|
+
|
|
198
292
|
function appendSkillSignal(signal) {
|
|
199
293
|
if (!signal) return;
|
|
200
294
|
try {
|
|
201
|
-
|
|
295
|
+
const payload = JSON.stringify(signal);
|
|
296
|
+
const policy = loadPolicy();
|
|
202
297
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
});
|
|
205
325
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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');
|
|
212
335
|
}
|
|
213
336
|
} catch {
|
|
214
337
|
// Non-fatal
|
|
@@ -261,7 +384,12 @@ function checkHotEvolution(signal) {
|
|
|
261
384
|
}
|
|
262
385
|
|
|
263
386
|
// Rule 2: User complaints about a skill
|
|
264
|
-
|
|
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
|
+
}
|
|
265
393
|
if (signal.prompt && complaintRe.test(signal.prompt) && signal.skills_invoked.length > 0) {
|
|
266
394
|
for (const sk of signal.skills_invoked) {
|
|
267
395
|
addToQueue(queue, {
|
|
@@ -274,8 +402,18 @@ function checkHotEvolution(signal) {
|
|
|
274
402
|
}
|
|
275
403
|
|
|
276
404
|
// Rule 3: Missing skill detection
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|
|
411
|
+
const missText = [(signal.error || ''), (signal.prompt || ''), (signal.output_excerpt || '')].join('\n');
|
|
412
|
+
const hasMissingPattern = missingRe.test(missText);
|
|
413
|
+
const hasExplicitSkillMiss = signal.has_tool_failure &&
|
|
414
|
+
/skill|技能|能力/i.test(missText) &&
|
|
415
|
+
/not found|missing|absent|no skill|找不到|没有找到|能力不足/i.test(missText);
|
|
416
|
+
if (signal.prompt && (hasMissingPattern || hasExplicitSkillMiss)) {
|
|
279
417
|
addToQueue(queue, {
|
|
280
418
|
type: 'skill_gap',
|
|
281
419
|
skill_name: null,
|
|
@@ -295,7 +433,7 @@ function checkHotEvolution(signal) {
|
|
|
295
433
|
if (isSuccess || isFail) {
|
|
296
434
|
for (const sk of signal.skills_invoked) {
|
|
297
435
|
const skillDir = findSkillDir(sk);
|
|
298
|
-
if (skillDir) trackInsightOutcome(skillDir, isSuccess);
|
|
436
|
+
if (skillDir) trackInsightOutcome(skillDir, isSuccess, signal);
|
|
299
437
|
}
|
|
300
438
|
}
|
|
301
439
|
}
|
|
@@ -305,7 +443,51 @@ function checkHotEvolution(signal) {
|
|
|
305
443
|
* Update insight outcome stats in evolution.json for a skill.
|
|
306
444
|
* Tracks success_count, fail_count, last_applied_at per insight text.
|
|
307
445
|
*/
|
|
308
|
-
|
|
446
|
+
const INSIGHT_STOPWORDS = new Set([
|
|
447
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'when', 'where',
|
|
448
|
+
'user', 'skill', 'meta', 'metame', 'should', 'always', 'never', 'please',
|
|
449
|
+
'问题', '用户', '技能', '需要', '应该', '这个', '那个', '以及', '如果', '然后',
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
function extractInsightTokens(text) {
|
|
453
|
+
const raw = String(text || '').toLowerCase();
|
|
454
|
+
const en = raw.match(/[a-z][a-z0-9_.-]{2,}/g) || [];
|
|
455
|
+
const zh = raw.match(/[\u4e00-\u9fff]{2,}/g) || [];
|
|
456
|
+
const tokens = [...en, ...zh]
|
|
457
|
+
.map(t => t.trim())
|
|
458
|
+
.filter(t => t.length >= 2 && !INSIGHT_STOPWORDS.has(t));
|
|
459
|
+
return [...new Set(tokens)];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function pickMatchedInsights(allInsights, signal) {
|
|
463
|
+
if (!signal || !Array.isArray(allInsights) || allInsights.length === 0) return [];
|
|
464
|
+
|
|
465
|
+
const context = [
|
|
466
|
+
signal.prompt || '',
|
|
467
|
+
signal.error || '',
|
|
468
|
+
signal.output_excerpt || '',
|
|
469
|
+
...(Array.isArray(signal.tools_used) ? signal.tools_used.map(t => `${t.name || ''} ${t.context || ''}`) : []),
|
|
470
|
+
...(Array.isArray(signal.files_modified) ? signal.files_modified : []),
|
|
471
|
+
].join('\n').toLowerCase();
|
|
472
|
+
|
|
473
|
+
const scored = allInsights
|
|
474
|
+
.map((insight) => {
|
|
475
|
+
const tokens = extractInsightTokens(insight).slice(0, 12);
|
|
476
|
+
if (tokens.length === 0) return { insight, score: 0 };
|
|
477
|
+
let score = 0;
|
|
478
|
+
for (const token of tokens) {
|
|
479
|
+
if (context.includes(token)) score++;
|
|
480
|
+
}
|
|
481
|
+
return { insight, score };
|
|
482
|
+
})
|
|
483
|
+
.filter(x => x.score > 0)
|
|
484
|
+
.sort((a, b) => b.score - a.score);
|
|
485
|
+
|
|
486
|
+
if (scored.length > 0) return scored.slice(0, 3).map(x => x.insight);
|
|
487
|
+
return allInsights.length === 1 ? [allInsights[0]] : [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function trackInsightOutcome(skillDir, isSuccess, signal = null) {
|
|
309
491
|
const evoPath = path.join(skillDir, 'evolution.json');
|
|
310
492
|
let data = {};
|
|
311
493
|
try { data = JSON.parse(fs.readFileSync(evoPath, 'utf8')); } catch { return; }
|
|
@@ -317,8 +499,9 @@ function trackInsightOutcome(skillDir, isSuccess) {
|
|
|
317
499
|
...(data.fixes || []),
|
|
318
500
|
...(data.contexts || []),
|
|
319
501
|
];
|
|
502
|
+
const targetInsights = signal ? pickMatchedInsights(allInsights, signal) : allInsights;
|
|
320
503
|
|
|
321
|
-
for (const insight of
|
|
504
|
+
for (const insight of targetInsights) {
|
|
322
505
|
if (!data.insights_stats[insight]) {
|
|
323
506
|
data.insights_stats[insight] = { success_count: 0, fail_count: 0, last_applied_at: null };
|
|
324
507
|
}
|
|
@@ -333,7 +516,7 @@ function trackInsightOutcome(skillDir, isSuccess) {
|
|
|
333
516
|
|
|
334
517
|
// ─────────────────────────────────────────────
|
|
335
518
|
// Cold Path: Haiku-Powered Analysis
|
|
336
|
-
// (called
|
|
519
|
+
// (called by heartbeat script task: skill-evolve)
|
|
337
520
|
// ─────────────────────────────────────────────
|
|
338
521
|
|
|
339
522
|
/**
|
|
@@ -362,7 +545,6 @@ async function distillSkills() {
|
|
|
362
545
|
// Get installed skills list
|
|
363
546
|
const installedSkills = listInstalledSkills();
|
|
364
547
|
if (installedSkills.length === 0) {
|
|
365
|
-
clearSignals();
|
|
366
548
|
return null;
|
|
367
549
|
}
|
|
368
550
|
|
|
@@ -414,7 +596,6 @@ async function distillSkills() {
|
|
|
414
596
|
|
|
415
597
|
const jsonMatch = result.match(/```json\s*([\s\S]*?)```/);
|
|
416
598
|
if (!jsonMatch) {
|
|
417
|
-
clearSignals();
|
|
418
599
|
return null;
|
|
419
600
|
}
|
|
420
601
|
|
|
@@ -572,8 +753,15 @@ RULES:
|
|
|
572
753
|
const patchData = yaml.load(yamlMatch[1]);
|
|
573
754
|
if (!patchData || typeof patchData !== 'object') return;
|
|
574
755
|
|
|
575
|
-
// Apply patch
|
|
576
|
-
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
|
+
}
|
|
577
765
|
savePolicy(yaml, newPolicy);
|
|
578
766
|
console.log(`🧬 Policy self-evolved: v${policy.version} → v${newPolicy.version}`);
|
|
579
767
|
|
|
@@ -591,7 +779,16 @@ function loadEvolutionQueue(yaml) {
|
|
|
591
779
|
if (!fs.existsSync(EVOLUTION_QUEUE_FILE)) return { items: [] };
|
|
592
780
|
const content = fs.readFileSync(EVOLUTION_QUEUE_FILE, 'utf8');
|
|
593
781
|
const data = yaml.load(content);
|
|
594
|
-
|
|
782
|
+
const queue = data && Array.isArray(data.items) ? data : { items: [] };
|
|
783
|
+
let changed = false;
|
|
784
|
+
for (const item of queue.items) {
|
|
785
|
+
if (!item.id) {
|
|
786
|
+
item.id = `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
787
|
+
changed = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (changed) saveEvolutionQueue(yaml, queue);
|
|
791
|
+
return queue;
|
|
595
792
|
} catch {
|
|
596
793
|
return { items: [] };
|
|
597
794
|
}
|
|
@@ -604,18 +801,24 @@ function saveEvolutionQueue(yaml, queue) {
|
|
|
604
801
|
}
|
|
605
802
|
|
|
606
803
|
function addToQueue(queue, entry) {
|
|
607
|
-
// Dedup by
|
|
804
|
+
// Dedup pending entries by core key. Skill gaps also include search_hint so unrelated gaps don't collapse.
|
|
608
805
|
const existing = queue.items.find(i =>
|
|
609
|
-
i.type === entry.type &&
|
|
806
|
+
i.type === entry.type &&
|
|
807
|
+
i.skill_name === entry.skill_name &&
|
|
808
|
+
i.status === 'pending' &&
|
|
809
|
+
(entry.type !== 'skill_gap' || (i.search_hint || '') === (entry.search_hint || ''))
|
|
610
810
|
);
|
|
611
811
|
|
|
612
812
|
if (existing) {
|
|
613
813
|
existing.evidence_count = (existing.evidence_count || 0) + (entry.evidence_count || 1);
|
|
614
814
|
existing.last_seen = new Date().toISOString();
|
|
815
|
+
if (entry.reason) existing.reason = entry.reason;
|
|
816
|
+
if (entry.search_hint) existing.search_hint = entry.search_hint;
|
|
615
817
|
return;
|
|
616
818
|
}
|
|
617
819
|
|
|
618
820
|
queue.items.push({
|
|
821
|
+
id: `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
619
822
|
...entry,
|
|
620
823
|
detected: new Date().toISOString(),
|
|
621
824
|
last_seen: new Date().toISOString(),
|
|
@@ -639,13 +842,11 @@ function checkEvolutionQueue() {
|
|
|
639
842
|
|
|
640
843
|
const queue = loadEvolutionQueue(yaml);
|
|
641
844
|
const pendingItems = queue.items.filter(i => i.status === 'pending');
|
|
642
|
-
if (pendingItems.length === 0) return [];
|
|
643
|
-
|
|
644
845
|
const notifications = [];
|
|
846
|
+
const policy = loadPolicy();
|
|
645
847
|
|
|
646
848
|
for (const item of pendingItems) {
|
|
647
849
|
// Require minimum evidence before notifying
|
|
648
|
-
const policy = loadPolicy();
|
|
649
850
|
const minEvidence = item.type === 'skill_gap' ? policy.min_evidence_for_gap : policy.min_evidence_for_update;
|
|
650
851
|
if ((item.evidence_count || 1) < minEvidence) continue;
|
|
651
852
|
|
|
@@ -655,13 +856,14 @@ function checkEvolutionQueue() {
|
|
|
655
856
|
}
|
|
656
857
|
|
|
657
858
|
// Prune old resolved items (> 30 days)
|
|
859
|
+
const beforeLen = queue.items.length;
|
|
658
860
|
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
659
861
|
queue.items = queue.items.filter(i =>
|
|
660
862
|
i.status === 'pending' || i.status === 'notified' ||
|
|
661
863
|
(new Date(i.last_seen || i.detected).getTime() > cutoff)
|
|
662
864
|
);
|
|
663
865
|
|
|
664
|
-
if (notifications.length > 0) {
|
|
866
|
+
if (notifications.length > 0 || queue.items.length !== beforeLen) {
|
|
665
867
|
saveEvolutionQueue(yaml, queue);
|
|
666
868
|
}
|
|
667
869
|
|
|
@@ -687,6 +889,42 @@ function resolveQueueItem(type, skillName, resolution) {
|
|
|
687
889
|
}
|
|
688
890
|
}
|
|
689
891
|
|
|
892
|
+
/**
|
|
893
|
+
* Mark queue item resolved by queue id.
|
|
894
|
+
* Returns true when updated.
|
|
895
|
+
*/
|
|
896
|
+
function resolveQueueItemById(id, resolution) {
|
|
897
|
+
let yaml;
|
|
898
|
+
try { yaml = require('js-yaml'); } catch { return false; }
|
|
899
|
+
if (!id) return false;
|
|
900
|
+
|
|
901
|
+
const queue = loadEvolutionQueue(yaml);
|
|
902
|
+
const item = queue.items.find(i =>
|
|
903
|
+
i.id === id && (i.status === 'pending' || i.status === 'notified')
|
|
904
|
+
);
|
|
905
|
+
if (!item) return false;
|
|
906
|
+
|
|
907
|
+
item.status = resolution; // 'installed' | 'dismissed'
|
|
908
|
+
item.resolved_at = new Date().toISOString();
|
|
909
|
+
saveEvolutionQueue(yaml, queue);
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* List queue items for manual triage.
|
|
915
|
+
*/
|
|
916
|
+
function listQueueItems({ status = null, limit = 20 } = {}) {
|
|
917
|
+
let yaml;
|
|
918
|
+
try { yaml = require('js-yaml'); } catch { return []; }
|
|
919
|
+
const queue = loadEvolutionQueue(yaml);
|
|
920
|
+
const items = Array.isArray(queue.items) ? queue.items : [];
|
|
921
|
+
const filtered = status ? items.filter(i => i.status === status) : items;
|
|
922
|
+
return filtered
|
|
923
|
+
.slice()
|
|
924
|
+
.sort((a, b) => new Date(b.last_seen || b.detected || 0).getTime() - new Date(a.last_seen || a.detected || 0).getTime())
|
|
925
|
+
.slice(0, Math.max(1, limit));
|
|
926
|
+
}
|
|
927
|
+
|
|
690
928
|
// ─────────────────────────────────────────────
|
|
691
929
|
// Evolution.json Merge (JS port of merge_evolution.py)
|
|
692
930
|
// ─────────────────────────────────────────────
|
|
@@ -726,8 +964,11 @@ function smartStitch(skillDir) {
|
|
|
726
964
|
try { data = JSON.parse(fs.readFileSync(evoPath, 'utf8')); } catch { return; }
|
|
727
965
|
|
|
728
966
|
// Build evolution section
|
|
967
|
+
const AUTO_START = '<!-- METAME-EVOLUTION:START -->';
|
|
968
|
+
const AUTO_END = '<!-- METAME-EVOLUTION:END -->';
|
|
729
969
|
const sections = [];
|
|
730
|
-
sections.push(
|
|
970
|
+
sections.push(`\n\n${AUTO_START}`);
|
|
971
|
+
sections.push('\n## User-Learned Best Practices & Constraints');
|
|
731
972
|
sections.push('\n> **Auto-Generated Section**: Maintained by skill-evolution-manager. Do not edit manually.');
|
|
732
973
|
|
|
733
974
|
// Helper: get quality indicator for an insight based on stats
|
|
@@ -755,13 +996,17 @@ function smartStitch(skillDir) {
|
|
|
755
996
|
sections.push(`\n${data.custom_prompts}`);
|
|
756
997
|
}
|
|
757
998
|
|
|
999
|
+
sections.push(`\n${AUTO_END}\n`);
|
|
758
1000
|
const evolutionBlock = sections.join('\n');
|
|
759
1001
|
|
|
760
1002
|
let content = fs.readFileSync(skillMdPath, 'utf8');
|
|
761
|
-
const
|
|
1003
|
+
const markerPattern = new RegExp(`${AUTO_START}[\\s\\S]*?${AUTO_END}\\n?`);
|
|
1004
|
+
const legacyPattern = /\n+## User-Learned Best Practices & Constraints[\s\S]*?(?=\n##\s+|\n#\s+|$)/;
|
|
762
1005
|
|
|
763
|
-
if (
|
|
764
|
-
content = content.replace(
|
|
1006
|
+
if (markerPattern.test(content)) {
|
|
1007
|
+
content = content.replace(markerPattern, evolutionBlock.trimStart());
|
|
1008
|
+
} else if (legacyPattern.test(content)) {
|
|
1009
|
+
content = content.replace(legacyPattern, evolutionBlock);
|
|
765
1010
|
} else {
|
|
766
1011
|
content = content + evolutionBlock;
|
|
767
1012
|
}
|
|
@@ -787,7 +1032,10 @@ function readRecentSignals() {
|
|
|
787
1032
|
}
|
|
788
1033
|
|
|
789
1034
|
function clearSignals() {
|
|
790
|
-
try {
|
|
1035
|
+
try {
|
|
1036
|
+
const locked = withSkillSignalLock(() => writeSkillSignalLines([]));
|
|
1037
|
+
if (!locked) fs.writeFileSync(SKILL_SIGNAL_FILE, '', 'utf8');
|
|
1038
|
+
} catch {}
|
|
791
1039
|
}
|
|
792
1040
|
|
|
793
1041
|
function listInstalledSkills() {
|
|
@@ -827,6 +1075,8 @@ module.exports = {
|
|
|
827
1075
|
distillSkills,
|
|
828
1076
|
checkEvolutionQueue,
|
|
829
1077
|
resolveQueueItem,
|
|
1078
|
+
resolveQueueItemById,
|
|
1079
|
+
listQueueItems,
|
|
830
1080
|
mergeEvolution,
|
|
831
1081
|
smartStitch,
|
|
832
1082
|
trackInsightOutcome,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function mkHome() {
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'metame-skill-evo-'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function runWithHome(home, code) {
|
|
16
|
+
return execFileSync(process.execPath, ['-e', code], {
|
|
17
|
+
cwd: ROOT,
|
|
18
|
+
env: { ...process.env, HOME: home },
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('captures missing-skill failures into skill_gap queue', () => {
|
|
24
|
+
const home = mkHome();
|
|
25
|
+
runWithHome(home, `
|
|
26
|
+
const se = require('./scripts/skill-evolution');
|
|
27
|
+
const s = se.extractSkillSignal('帮我做封面图', 'Error: skill not found: nano-banana', null, [], process.cwd(), []);
|
|
28
|
+
se.appendSkillSignal(s);
|
|
29
|
+
se.checkHotEvolution(s);
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
const queuePath = path.join(home, '.metame', 'evolution_queue.yaml');
|
|
33
|
+
const queue = yaml.load(fs.readFileSync(queuePath, 'utf8'));
|
|
34
|
+
assert.equal(Array.isArray(queue.items), true);
|
|
35
|
+
assert.equal(queue.items.length, 1);
|
|
36
|
+
assert.equal(queue.items[0].type, 'skill_gap');
|
|
37
|
+
assert.equal(queue.items[0].status, 'pending');
|
|
38
|
+
assert.ok(queue.items[0].id);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('resolves queue item by id', () => {
|
|
42
|
+
const home = mkHome();
|
|
43
|
+
runWithHome(home, `
|
|
44
|
+
const se = require('./scripts/skill-evolution');
|
|
45
|
+
const s = se.extractSkillSignal('帮我做封面图', 'Error: skill not found: nano-banana', null, [], process.cwd(), []);
|
|
46
|
+
se.appendSkillSignal(s);
|
|
47
|
+
se.checkHotEvolution(s);
|
|
48
|
+
const items = se.listQueueItems({ status: 'pending', limit: 5 });
|
|
49
|
+
if (!items[0]) throw new Error('no pending queue item');
|
|
50
|
+
const ok = se.resolveQueueItemById(items[0].id, 'installed');
|
|
51
|
+
if (!ok) throw new Error('resolveQueueItemById returned false');
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
const queuePath = path.join(home, '.metame', 'evolution_queue.yaml');
|
|
55
|
+
const queue = yaml.load(fs.readFileSync(queuePath, 'utf8'));
|
|
56
|
+
assert.equal(queue.items.length, 1);
|
|
57
|
+
assert.equal(queue.items[0].status, 'installed');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('smartStitch preserves sections after evolution block', () => {
|
|
61
|
+
const home = mkHome();
|
|
62
|
+
runWithHome(home, `
|
|
63
|
+
const fs = require('fs');
|
|
64
|
+
const path = require('path');
|
|
65
|
+
const se = require('./scripts/skill-evolution');
|
|
66
|
+
const dir = path.join(process.env.HOME, 'sample-skill');
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), '# Demo\\n\\nBody\\n\\n## User-Learned Best Practices & Constraints\\nOld\\n\\n## KeepMe\\nKeep this section\\n');
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'evolution.json'), JSON.stringify({ preferences: ['prefer concise output'] }, null, 2));
|
|
70
|
+
se.smartStitch(dir);
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(path.join(home, 'sample-skill', 'SKILL.md'), 'utf8');
|
|
74
|
+
assert.match(content, /METAME-EVOLUTION:START/);
|
|
75
|
+
assert.match(content, /prefer concise output/);
|
|
76
|
+
assert.match(content, /## KeepMe/);
|
|
77
|
+
assert.match(content, /Keep this section/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('trackInsightOutcome updates only matched insights', () => {
|
|
81
|
+
const home = mkHome();
|
|
82
|
+
runWithHome(home, `
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
const path = require('path');
|
|
85
|
+
const se = require('./scripts/skill-evolution');
|
|
86
|
+
const dir = path.join(process.env.HOME, '.claude', 'skills', 'demo-skill');
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), '# Demo');
|
|
89
|
+
fs.writeFileSync(path.join(dir, 'evolution.json'), JSON.stringify({
|
|
90
|
+
preferences: ['alpha_mode'],
|
|
91
|
+
fixes: ['beta_mode']
|
|
92
|
+
}, null, 2));
|
|
93
|
+
se.trackInsightOutcome(dir, true, {
|
|
94
|
+
prompt: 'please use alpha_mode',
|
|
95
|
+
error: '',
|
|
96
|
+
output_excerpt: '',
|
|
97
|
+
tools_used: [],
|
|
98
|
+
files_modified: []
|
|
99
|
+
});
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
const evo = JSON.parse(fs.readFileSync(path.join(home, '.claude', 'skills', 'demo-skill', 'evolution.json'), 'utf8'));
|
|
103
|
+
assert.equal(typeof evo.insights_stats, 'object');
|
|
104
|
+
assert.ok(evo.insights_stats.alpha_mode);
|
|
105
|
+
assert.equal(evo.insights_stats.alpha_mode.success_count, 1);
|
|
106
|
+
assert.equal(evo.insights_stats.beta_mode, undefined);
|
|
107
|
+
});
|