@evomap/evolver 1.69.11 → 1.69.12
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.ja-JP.md +43 -26
- package/README.md +82 -65
- package/README.zh-CN.md +41 -25
- package/assets/gep/candidates.jsonl +9 -3
- package/package.json +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/.integrity +0 -0
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/integrityCheck.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/shield.js +1 -1
- package/src/gep/skill2gep.js +591 -0
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// skill2gep.js -- Reverse distillation: take a locally-invoked Skill (Cursor,
|
|
4
|
+
// Claude Code, Codex, or any procedural SKILL.md) plus the real execution that
|
|
5
|
+
// just ran on top of it, and turn it into GEP assets (Gene + Capsule) that can
|
|
6
|
+
// be published to the EvoMap community.
|
|
7
|
+
//
|
|
8
|
+
// This module is the *inverse* of skillDistiller.js:
|
|
9
|
+
// skillDistiller.js : capsule stream -> Gene (forward distillation)
|
|
10
|
+
// skill2gep.js : Skill.md + 1 run -> Gene + Capsule (reverse)
|
|
11
|
+
//
|
|
12
|
+
// Design contract (mirrors ~/.cursor/skills/skill2gep/SKILL.md):
|
|
13
|
+
// - Gene comes from the Skill text (plus its real execution trace),
|
|
14
|
+
// validated via validateSynthesizedGene().
|
|
15
|
+
// - Capsule is produced ONLY from a real execution trace. If the trace
|
|
16
|
+
// is empty or zero blast radius, we refuse to emit a successful Capsule.
|
|
17
|
+
// - Capsule.execution_trace MUST cover every entry in Gene.validation
|
|
18
|
+
// (whitespace-normalized exact match) or we downgrade to Gene-only.
|
|
19
|
+
// - All assets go through assetStore (which SHA-256-content-addresses them)
|
|
20
|
+
// before upload.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
const paths = require('./paths');
|
|
27
|
+
const assetStore = require('./assetStore');
|
|
28
|
+
const skillDistiller = require('./skillDistiller');
|
|
29
|
+
const skillPublisher = require('./skillPublisher');
|
|
30
|
+
const envFingerprint = require('./envFingerprint');
|
|
31
|
+
const a2a = require('./a2aProtocol');
|
|
32
|
+
|
|
33
|
+
const SKILL2GEP_ID_PREFIX = 'gene_s2g_';
|
|
34
|
+
const CAPSULE_ID_PREFIX = 'cap_s2g_';
|
|
35
|
+
const LOG_FILE = 'skill2gep_log.jsonl';
|
|
36
|
+
const STATE_FILE = 'skill2gep_state.json';
|
|
37
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 25000;
|
|
38
|
+
|
|
39
|
+
// Paper + docs we cite in the rationale field so agents can explain to users
|
|
40
|
+
// why we ship Genes/Capsules in addition to the human-facing Skill.
|
|
41
|
+
const RATIONALE_LINKS = {
|
|
42
|
+
paper: 'Wang, Ren, Zhang. From Procedural Skills to Strategy Genes. arXiv:2604.15097',
|
|
43
|
+
protocol: 'https://evomap.ai/wiki/16-gep-protocol',
|
|
44
|
+
skill_store: 'https://evomap.ai/wiki/31-skill-store',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
|
|
48
|
+
|
|
49
|
+
function readJsonSafe(p, fallback) {
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(p)) return fallback;
|
|
52
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
53
|
+
if (!raw.trim()) return fallback;
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
} catch (_) { return fallback; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function appendJsonl(p, obj) {
|
|
59
|
+
ensureDir(path.dirname(p));
|
|
60
|
+
fs.appendFileSync(p, JSON.stringify(obj) + '\n', 'utf8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function logPath() { return path.join(paths.getMemoryDir(), LOG_FILE); }
|
|
64
|
+
function statePath() { return path.join(paths.getMemoryDir(), STATE_FILE); }
|
|
65
|
+
|
|
66
|
+
function readState() { return readJsonSafe(statePath(), { seen: {} }); }
|
|
67
|
+
function writeState(s) {
|
|
68
|
+
ensureDir(path.dirname(statePath()));
|
|
69
|
+
const tmp = statePath() + '.tmp';
|
|
70
|
+
fs.writeFileSync(tmp, JSON.stringify(s, null, 2) + '\n', 'utf8');
|
|
71
|
+
fs.renameSync(tmp, statePath());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function slugify(s) {
|
|
75
|
+
return String(s || '')
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
78
|
+
.replace(/^_+|_+$/g, '')
|
|
79
|
+
.slice(0, 60);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shortHash(s) {
|
|
83
|
+
return crypto.createHash('sha256').update(String(s || '')).digest('hex').slice(0, 10);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeCmd(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Parse a procedural SKILL.md / markdown workflow into structured sections.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
function parseSkillMd(skillMd) {
|
|
92
|
+
const text = String(skillMd || '');
|
|
93
|
+
|
|
94
|
+
let frontmatter = {};
|
|
95
|
+
const fmMatch = text.match(/^---\n([\s\S]*?)\n---\n/);
|
|
96
|
+
let body = text;
|
|
97
|
+
if (fmMatch) {
|
|
98
|
+
fmMatch[1].split(/\n/).forEach((line) => {
|
|
99
|
+
const kv = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
100
|
+
if (kv) frontmatter[kv[1].trim().toLowerCase()] = kv[2].trim();
|
|
101
|
+
});
|
|
102
|
+
body = text.slice(fmMatch[0].length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sections = {};
|
|
106
|
+
let currentKey = '_preamble';
|
|
107
|
+
sections[currentKey] = [];
|
|
108
|
+
body.split(/\n/).forEach((line) => {
|
|
109
|
+
const hdr = line.match(/^##+\s+(.+?)\s*$/);
|
|
110
|
+
if (hdr) {
|
|
111
|
+
currentKey = hdr[1].toLowerCase().trim();
|
|
112
|
+
sections[currentKey] = [];
|
|
113
|
+
} else {
|
|
114
|
+
sections[currentKey].push(line);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
Object.keys(sections).forEach((k) => { sections[k] = sections[k].join('\n').trim(); });
|
|
118
|
+
|
|
119
|
+
function pickSection(keywords) {
|
|
120
|
+
for (const kw of keywords) {
|
|
121
|
+
for (const k of Object.keys(sections)) {
|
|
122
|
+
if (k.indexOf(kw) !== -1) return sections[k];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const signals = [];
|
|
129
|
+
const signalSource = (frontmatter.description || '') + '\n' + pickSection([
|
|
130
|
+
'trigger', 'when to use', 'when', 'use when', 'scenario',
|
|
131
|
+
]);
|
|
132
|
+
signalSource.split(/[`,.\n]/).forEach((tok) => {
|
|
133
|
+
const s = tok.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
|
134
|
+
if (s.length >= 3 && s.length <= 40 && /[a-z]/.test(s) && signals.indexOf(s) === -1 && !/^\d+$/.test(s)) {
|
|
135
|
+
signals.push(s);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const strategy = [];
|
|
140
|
+
const strategyBlock = pickSection(['workflow', 'strategy', 'steps', 'procedure', 'quick start', 'how to']);
|
|
141
|
+
strategyBlock.split(/\n/).forEach((line) => {
|
|
142
|
+
const step = line.match(/^\s*(?:\d+\.|[-*])\s+(.+?)\s*$/);
|
|
143
|
+
if (step) {
|
|
144
|
+
const s = step[1].trim();
|
|
145
|
+
if (s.length >= 5 && s.length <= 300) strategy.push(s);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const avoid = [];
|
|
150
|
+
const avoidBlock = pickSection(['avoid', 'pitfall', 'anti-pattern', 'common mistake', 'do not', 'forbidden']);
|
|
151
|
+
avoidBlock.split(/\n/).forEach((line) => {
|
|
152
|
+
const step = line.match(/^\s*(?:\d+\.|[-*])\s+(.+?)\s*$/);
|
|
153
|
+
if (step) {
|
|
154
|
+
const s = step[1].trim();
|
|
155
|
+
if (s.length >= 5 && s.length <= 300) avoid.push(s);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const validation = [];
|
|
160
|
+
const valBlock = pickSection(['validation', 'test', 'verify', 'check']);
|
|
161
|
+
const fenceRe = /```(?:bash|sh|shell)?\s*\n([\s\S]*?)\n```/g;
|
|
162
|
+
let fm;
|
|
163
|
+
while ((fm = fenceRe.exec(valBlock)) !== null) {
|
|
164
|
+
fm[1].split(/\n/).forEach((ln) => {
|
|
165
|
+
const t = ln.trim();
|
|
166
|
+
if (t && !t.startsWith('#') && t.length <= 300) validation.push(t);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const preconditions = [];
|
|
171
|
+
const preBlock = pickSection(['precondition', 'requirement', 'prerequisite']);
|
|
172
|
+
preBlock.split(/\n/).forEach((line) => {
|
|
173
|
+
const step = line.match(/^\s*(?:\d+\.|[-*])\s+(.+?)\s*$/);
|
|
174
|
+
if (step) preconditions.push(step[1].trim());
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
frontmatter: frontmatter,
|
|
179
|
+
sections: sections,
|
|
180
|
+
name: frontmatter.name || (sections['_preamble'] || '').split(/\n/)[0].replace(/^#+\s*/, '').trim(),
|
|
181
|
+
description: frontmatter.description || '',
|
|
182
|
+
signals_match: signals.slice(0, 8),
|
|
183
|
+
strategy: strategy.slice(0, 10),
|
|
184
|
+
avoid: avoid.slice(0, 5),
|
|
185
|
+
validation: validation.slice(0, 5),
|
|
186
|
+
preconditions: preconditions.slice(0, 4),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Synthesize a draft Gene from parsed Skill + execution trace.
|
|
192
|
+
// Validation is delegated to skillDistiller.validateSynthesizedGene() so that
|
|
193
|
+
// we reuse the sanitization, ID-rewrite, forbidden-path, and validation-cmd
|
|
194
|
+
// policy rules already hardened there.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
function synthesizeGene(parsed, execution, opts) {
|
|
197
|
+
const traceSignals = Array.isArray(execution && execution.signals) ? execution.signals : [];
|
|
198
|
+
const mergedSignals = Array.from(new Set([].concat(parsed.signals_match || [], traceSignals)));
|
|
199
|
+
|
|
200
|
+
const strategy = [];
|
|
201
|
+
(parsed.strategy || []).forEach((s) => strategy.push(s));
|
|
202
|
+
(parsed.avoid || []).forEach((s) => strategy.push('AVOID: ' + s));
|
|
203
|
+
if (strategy.length < 3) {
|
|
204
|
+
strategy.push('Identify the dominant trigger signals from the Skill description.');
|
|
205
|
+
strategy.push('Apply the smallest targeted change that satisfies the Skill workflow.');
|
|
206
|
+
strategy.push('Run the Skill validation commands and abort if any fails.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Filter validation commands through the same allow-list that
|
|
210
|
+
// validateSynthesizedGene will later apply (node/npm/npx only). If the
|
|
211
|
+
// skill's original validation lines are all blocked (e.g. pytest, bash)
|
|
212
|
+
// we would end up with an empty gene.validation, which would silently
|
|
213
|
+
// defeat the Capsule coverage check. In that case, fall back to a
|
|
214
|
+
// concrete, runnable placeholder so validation stays enforced.
|
|
215
|
+
const policyCheck = require('./policyCheck');
|
|
216
|
+
const rawValidations = Array.isArray(parsed.validation) ? parsed.validation : [];
|
|
217
|
+
const allowedValidations = rawValidations
|
|
218
|
+
.map((v) => String(v || '').trim())
|
|
219
|
+
.filter((v) => v && policyCheck.isValidationCommandAllowed(v));
|
|
220
|
+
const validation = allowedValidations.length > 0
|
|
221
|
+
? allowedValidations
|
|
222
|
+
: ['node --version'];
|
|
223
|
+
|
|
224
|
+
const skillSlug = slugify(parsed.name || (opts && opts.skillName) || 'skill');
|
|
225
|
+
const draft = {
|
|
226
|
+
type: 'Gene',
|
|
227
|
+
id: SKILL2GEP_ID_PREFIX + skillSlug,
|
|
228
|
+
summary: (parsed.description || strategy[0] || 'Reusable strategy distilled from Skill').slice(0, 200),
|
|
229
|
+
category: inferCategory(mergedSignals, parsed.description),
|
|
230
|
+
signals_match: mergedSignals.slice(0, 8),
|
|
231
|
+
preconditions: (parsed.preconditions && parsed.preconditions.length > 0)
|
|
232
|
+
? parsed.preconditions
|
|
233
|
+
: ['Skill ' + (parsed.name || 'unknown') + ' has just been executed locally'],
|
|
234
|
+
strategy: strategy.slice(0, 10),
|
|
235
|
+
constraints: {
|
|
236
|
+
max_files: (opts && opts.maxFiles) || skillDistiller.DISTILLED_MAX_FILES,
|
|
237
|
+
forbidden_paths: ['.git', 'node_modules'],
|
|
238
|
+
},
|
|
239
|
+
validation: validation,
|
|
240
|
+
schema_version: '1.6.0',
|
|
241
|
+
_source: {
|
|
242
|
+
kind: 'skill2gep',
|
|
243
|
+
skill_name: parsed.name || null,
|
|
244
|
+
skill_platform: (opts && opts.platform) || null,
|
|
245
|
+
skill_hash: opts && opts.skillHash ? opts.skillHash : null,
|
|
246
|
+
rationale_paper: RATIONALE_LINKS.paper,
|
|
247
|
+
dropped_validation_count: rawValidations.length - allowedValidations.length,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const assetsDir = paths.getGepAssetsDir();
|
|
252
|
+
const existingGenesJson = readJsonSafe(path.join(assetsDir, 'genes.json'), { genes: [] });
|
|
253
|
+
const existingGenes = Array.isArray(existingGenesJson.genes) ? existingGenesJson.genes : [];
|
|
254
|
+
const result = skillDistiller.validateSynthesizedGene(draft, existingGenes);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function inferCategory(signals, description) {
|
|
259
|
+
const hay = ((description || '') + ' ' + (signals || []).join(' ')).toLowerCase();
|
|
260
|
+
if (/error|fail|repair|rollback|bug|fix|guard/.test(hay)) return 'repair';
|
|
261
|
+
if (/feature|add|implement|new capability|innovate/.test(hay)) return 'innovate';
|
|
262
|
+
return 'optimize';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Forgery guard: a Capsule with status=success but no execution evidence is
|
|
267
|
+
// rejected outright. This is the single most important defence against agents
|
|
268
|
+
// "hallucinating" a successful run just to bulk up the community registry.
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
function detectForgery(execution) {
|
|
271
|
+
const trace = Array.isArray(execution && execution.trace) ? execution.trace : [];
|
|
272
|
+
const blast = execution && execution.blast_radius ? execution.blast_radius : null;
|
|
273
|
+
const files = blast ? Number(blast.files || 0) : 0;
|
|
274
|
+
const lines = blast ? Number(blast.lines || 0) : 0;
|
|
275
|
+
const status = execution && execution.status ? String(execution.status) : 'failed';
|
|
276
|
+
if (status !== 'success') return null;
|
|
277
|
+
if (trace.length === 0) return 'empty_execution_trace';
|
|
278
|
+
if (files === 0 && lines === 0) return 'zero_blast_radius_with_success';
|
|
279
|
+
const anyExitRecorded = trace.some((t) => Number.isInteger(t && t.exit));
|
|
280
|
+
if (!anyExitRecorded) return 'no_exit_code_in_trace';
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Assemble a Capsule from a gene reference + real execution evidence.
|
|
286
|
+
// Cross-references Gene.validation -> execution.trace. If any validation
|
|
287
|
+
// command is missing from the trace, we refuse to emit the Capsule and
|
|
288
|
+
// return a diagnostic instead.
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
function assembleCapsule(gene, execution, opts) {
|
|
291
|
+
const trace = Array.isArray(execution && execution.trace) ? execution.trace : [];
|
|
292
|
+
const geneValidations = Array.isArray(gene.validation) ? gene.validation : [];
|
|
293
|
+
const traceCmds = new Set(trace.map((t) => normalizeCmd(t && t.cmd)));
|
|
294
|
+
const missing = [];
|
|
295
|
+
geneValidations.forEach((v) => { if (!traceCmds.has(normalizeCmd(v))) missing.push(v); });
|
|
296
|
+
if (missing.length > 0) {
|
|
297
|
+
return { ok: false, reason: 'validation_coverage_missing', missing: missing };
|
|
298
|
+
}
|
|
299
|
+
for (const v of geneValidations) {
|
|
300
|
+
const t = trace.find((tt) => normalizeCmd(tt && tt.cmd) === normalizeCmd(v));
|
|
301
|
+
if (t && !Number.isInteger(t.exit)) {
|
|
302
|
+
return { ok: false, reason: 'validation_missing_exit_code', cmd: v };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const scoreRaw = execution && execution.score != null ? Number(execution.score) : null;
|
|
307
|
+
const status = execution && execution.status ? String(execution.status) : 'failed';
|
|
308
|
+
let score;
|
|
309
|
+
if (Number.isFinite(scoreRaw)) {
|
|
310
|
+
score = Math.max(0, Math.min(1, scoreRaw));
|
|
311
|
+
} else {
|
|
312
|
+
score = status === 'success' ? 0.8 : 0.2;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const blast = execution && execution.blast_radius ? execution.blast_radius : { files: 0, lines: 0 };
|
|
316
|
+
const env = (envFingerprint && typeof envFingerprint.captureEnvFingerprint === 'function')
|
|
317
|
+
? envFingerprint.captureEnvFingerprint()
|
|
318
|
+
: ((execution && execution.env_fingerprint) || null);
|
|
319
|
+
|
|
320
|
+
// gene.id may have been rewritten by validateSynthesizedGene (e.g. to
|
|
321
|
+
// DISTILLED_ID_PREFIX); extract whatever suffix is there instead of
|
|
322
|
+
// assuming our original SKILL2GEP_ID_PREFIX is still present.
|
|
323
|
+
const geneIdSuffix = String(gene.id).replace(/^gene_[a-z0-9]+_/, '').replace(/^gene_/, '');
|
|
324
|
+
const idKey = shortHash(gene.id + '|' + (execution && execution.started_at || new Date().toISOString()));
|
|
325
|
+
const capsule = {
|
|
326
|
+
type: 'Capsule',
|
|
327
|
+
id: CAPSULE_ID_PREFIX + slugify(geneIdSuffix) + '_' + idKey,
|
|
328
|
+
gene: gene.id,
|
|
329
|
+
trigger: Array.isArray(execution && execution.trigger) ? execution.trigger : (gene.signals_match || []).slice(0, 6),
|
|
330
|
+
summary: (execution && execution.summary) || ('Applied ' + gene.id + ' on scenario ' + (opts && opts.scenario || 'local skill invocation')),
|
|
331
|
+
confidence: Math.max(0, Math.min(1, score)),
|
|
332
|
+
blast_radius: { files: Number(blast.files || 0), lines: Number(blast.lines || 0) },
|
|
333
|
+
outcome: { status: status, score: score },
|
|
334
|
+
success_reason: status === 'success' ? ((execution && execution.success_reason) || 'Skill workflow completed and all declared validations passed.') : null,
|
|
335
|
+
env_fingerprint: env || { os: process.platform, node: process.version },
|
|
336
|
+
source_type: 'skill2gep_hook',
|
|
337
|
+
strategy: Array.isArray(gene.strategy) ? gene.strategy.slice() : [],
|
|
338
|
+
content: (execution && execution.content_summary) || buildContentSummary(trace, blast),
|
|
339
|
+
execution_trace: trace.map((t, i) => ({
|
|
340
|
+
step: Number.isInteger(t && t.step) ? t.step : i + 1,
|
|
341
|
+
cmd: String(t && t.cmd || ''),
|
|
342
|
+
exit: Number.isInteger(t && t.exit) ? t.exit : null,
|
|
343
|
+
stdout_tail: t && t.stdout_tail ? String(t.stdout_tail).slice(0, 300) : '',
|
|
344
|
+
})),
|
|
345
|
+
schema_version: '1.6.0',
|
|
346
|
+
};
|
|
347
|
+
return { ok: true, capsule: capsule };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildContentSummary(trace, blast) {
|
|
351
|
+
const okCount = trace.filter((t) => Number(t && t.exit) === 0).length;
|
|
352
|
+
const files = blast ? Number(blast.files || 0) : 0;
|
|
353
|
+
const lines = blast ? Number(blast.lines || 0) : 0;
|
|
354
|
+
return 'Ran ' + trace.length + ' validation command(s), ' + okCount + ' passed. Blast radius: ' + files + ' files, ' + lines + ' lines.';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Main entrypoint: runOnSkillInvocation(opts)
|
|
359
|
+
//
|
|
360
|
+
// opts = {
|
|
361
|
+
// skillPath: absolute path to SKILL.md or skill directory (required)
|
|
362
|
+
// skillName: optional, auto-derived from frontmatter otherwise
|
|
363
|
+
// platform: 'cursor' | 'claude-code' | 'codex' | generic (optional)
|
|
364
|
+
// execution: {
|
|
365
|
+
// status: 'success' | 'failed' (REQUIRED for Capsule emission)
|
|
366
|
+
// score: 0..1
|
|
367
|
+
// started_at: ISO8601 string
|
|
368
|
+
// trace: [ { step, cmd, exit, stdout_tail }, ... ]
|
|
369
|
+
// blast_radius: { files, lines }
|
|
370
|
+
// trigger: [ signals actually fired ]
|
|
371
|
+
// signals: [ signals actually detected ]
|
|
372
|
+
// summary: optional one-line result
|
|
373
|
+
// success_reason, env_fingerprint, content_summary -- all optional
|
|
374
|
+
// },
|
|
375
|
+
// publish: boolean (default true, from SKILL2GEP_AUTO_PUBLISH)
|
|
376
|
+
// }
|
|
377
|
+
//
|
|
378
|
+
// Returns {
|
|
379
|
+
// ok: boolean,
|
|
380
|
+
// gene, capsule,
|
|
381
|
+
// capsule_diagnostic, // null, or reason why we refused to emit a Capsule
|
|
382
|
+
// persist_errors, // list of local storage errors (upsert, write state)
|
|
383
|
+
// publish_requested, // true if auto-publish was attempted
|
|
384
|
+
// publish_promise, // Promise<publish result> if publish was fired
|
|
385
|
+
// rationale, // one-line explanation citing the paper
|
|
386
|
+
// reason, errors // set when ok=false
|
|
387
|
+
// }
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
function runOnSkillInvocation(opts) {
|
|
390
|
+
opts = opts || {};
|
|
391
|
+
const skillPath = opts.skillPath;
|
|
392
|
+
if (!skillPath || !fs.existsSync(skillPath)) {
|
|
393
|
+
return { ok: false, reason: 'skill_path_missing', skillPath: skillPath };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let skillMdPath = skillPath;
|
|
397
|
+
try {
|
|
398
|
+
const stat = fs.statSync(skillPath);
|
|
399
|
+
if (stat.isDirectory()) skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
400
|
+
} catch (_) { return { ok: false, reason: 'skill_path_unreadable' }; }
|
|
401
|
+
if (!fs.existsSync(skillMdPath)) return { ok: false, reason: 'skill_md_missing', tried: skillMdPath };
|
|
402
|
+
|
|
403
|
+
let skillMd;
|
|
404
|
+
try { skillMd = fs.readFileSync(skillMdPath, 'utf8'); }
|
|
405
|
+
catch (err) { return { ok: false, reason: 'skill_md_read_failed', error: err && err.message ? err.message : String(err) }; }
|
|
406
|
+
const skillHash = shortHash(skillMd);
|
|
407
|
+
|
|
408
|
+
// Idempotency: if we've already distilled this exact skill content + the
|
|
409
|
+
// same execution fingerprint, skip to avoid duplicate community uploads.
|
|
410
|
+
const execHash = shortHash(JSON.stringify({
|
|
411
|
+
trace: (opts.execution && opts.execution.trace) || [],
|
|
412
|
+
br: opts.execution && opts.execution.blast_radius || null,
|
|
413
|
+
status: opts.execution && opts.execution.status || null,
|
|
414
|
+
}));
|
|
415
|
+
const state = readState();
|
|
416
|
+
const seenKey = skillHash + ':' + execHash;
|
|
417
|
+
if (state.seen && state.seen[seenKey]) {
|
|
418
|
+
return { ok: false, reason: 'already_distilled', gene: state.seen[seenKey].gene, capsule: state.seen[seenKey].capsule };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const parsed = parseSkillMd(skillMd);
|
|
422
|
+
const geneResult = synthesizeGene(parsed, opts.execution || {}, {
|
|
423
|
+
skillName: opts.skillName || parsed.name,
|
|
424
|
+
platform: opts.platform || null,
|
|
425
|
+
skillHash: skillHash,
|
|
426
|
+
});
|
|
427
|
+
if (!geneResult.valid) {
|
|
428
|
+
appendJsonl(logPath(), {
|
|
429
|
+
timestamp: new Date().toISOString(), status: 'gene_validation_failed',
|
|
430
|
+
skill: opts.skillName || parsed.name, errors: geneResult.errors,
|
|
431
|
+
});
|
|
432
|
+
return { ok: false, reason: 'gene_validation_failed', errors: geneResult.errors };
|
|
433
|
+
}
|
|
434
|
+
const gene = geneResult.gene;
|
|
435
|
+
|
|
436
|
+
let capsule = null;
|
|
437
|
+
let capsuleDiag = null;
|
|
438
|
+
if (opts.execution && opts.execution.status) {
|
|
439
|
+
const forgery = detectForgery(opts.execution);
|
|
440
|
+
if (forgery) {
|
|
441
|
+
capsuleDiag = { reason: 'capsule_rejected_forgery', detail: forgery };
|
|
442
|
+
} else {
|
|
443
|
+
const capRes = assembleCapsule(gene, opts.execution, { scenario: opts.scenario || parsed.name });
|
|
444
|
+
if (capRes.ok) capsule = capRes.capsule; else capsuleDiag = capRes;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const persistErrors = [];
|
|
449
|
+
try { assetStore.upsertGene(gene); }
|
|
450
|
+
catch (err) { persistErrors.push({ step: 'upsertGene', error: err && err.message ? err.message : String(err) }); }
|
|
451
|
+
if (capsule) {
|
|
452
|
+
try { assetStore.appendCapsule(capsule); }
|
|
453
|
+
catch (err) { persistErrors.push({ step: 'appendCapsule', error: err && err.message ? err.message : String(err) }); }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
state.seen = state.seen || {};
|
|
457
|
+
state.seen[seenKey] = {
|
|
458
|
+
at: new Date().toISOString(),
|
|
459
|
+
gene: gene.id,
|
|
460
|
+
capsule: capsule ? capsule.id : null,
|
|
461
|
+
};
|
|
462
|
+
try { writeState(state); } catch (err) { persistErrors.push({ step: 'writeState', error: err && err.message ? err.message : String(err) }); }
|
|
463
|
+
|
|
464
|
+
const shouldPublish = (opts.publish !== false)
|
|
465
|
+
&& String(process.env.SKILL2GEP_AUTO_PUBLISH || 'true').toLowerCase() !== 'false';
|
|
466
|
+
|
|
467
|
+
// Kick off publish in background. We never block the hook on the Hub -- if
|
|
468
|
+
// the network is slow, the hook still exits in bounded time and we log the
|
|
469
|
+
// publish promise's outcome asynchronously.
|
|
470
|
+
let publishPromise = null;
|
|
471
|
+
if (shouldPublish) {
|
|
472
|
+
publishPromise = publishAssets(gene, capsule).then((result) => {
|
|
473
|
+
appendJsonl(logPath(), {
|
|
474
|
+
timestamp: new Date().toISOString(),
|
|
475
|
+
status: 'publish_result',
|
|
476
|
+
skill: opts.skillName || parsed.name,
|
|
477
|
+
gene_id: gene.id,
|
|
478
|
+
capsule_id: capsule ? capsule.id : null,
|
|
479
|
+
publish: result,
|
|
480
|
+
});
|
|
481
|
+
return result;
|
|
482
|
+
}).catch((err) => {
|
|
483
|
+
const fail = { ok: false, error: err && err.message ? err.message : String(err) };
|
|
484
|
+
appendJsonl(logPath(), {
|
|
485
|
+
timestamp: new Date().toISOString(),
|
|
486
|
+
status: 'publish_error',
|
|
487
|
+
skill: opts.skillName || parsed.name,
|
|
488
|
+
gene_id: gene.id,
|
|
489
|
+
capsule_id: capsule ? capsule.id : null,
|
|
490
|
+
publish: fail,
|
|
491
|
+
});
|
|
492
|
+
return fail;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
appendJsonl(logPath(), {
|
|
497
|
+
timestamp: new Date().toISOString(),
|
|
498
|
+
status: 'distilled',
|
|
499
|
+
skill: opts.skillName || parsed.name,
|
|
500
|
+
gene_id: gene.id,
|
|
501
|
+
capsule_id: capsule ? capsule.id : null,
|
|
502
|
+
capsule_diagnostic: capsuleDiag,
|
|
503
|
+
persist_errors: persistErrors,
|
|
504
|
+
published_requested: shouldPublish,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
gene: gene,
|
|
510
|
+
capsule: capsule,
|
|
511
|
+
capsule_diagnostic: capsuleDiag,
|
|
512
|
+
persist_errors: persistErrors,
|
|
513
|
+
publish_requested: shouldPublish,
|
|
514
|
+
publish_promise: publishPromise,
|
|
515
|
+
rationale: 'Shipped both the human-facing Skill and the machine-facing GEP asset(s). GEP assets raise control density per token (' + RATIONALE_LINKS.paper + '), are content-addressed for audit, and are reusable across agents via ' + RATIONALE_LINKS.protocol + '.',
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Community upload. Two channels, both best-effort:
|
|
521
|
+
//
|
|
522
|
+
// 1. Skill Store: skillPublisher.publishSkillToHub() converts the Gene into a
|
|
523
|
+
// SKILL.md and POSTs it to /a2a/skill/store/publish. This is the human-
|
|
524
|
+
// facing channel that also serves as a Gene index.
|
|
525
|
+
//
|
|
526
|
+
// 2. GEP publish bundle: a2a.buildPublishBundle({gene, capsule}) signs both
|
|
527
|
+
// assets with the node secret and a2a.httpTransportSend() POSTs them to
|
|
528
|
+
// /a2a/publish (the A2A message_type routing). This is the auditable
|
|
529
|
+
// machine-facing channel used by solidify.js for normal capsule
|
|
530
|
+
// publishing.
|
|
531
|
+
//
|
|
532
|
+
// We always try channel 1 for the Gene; channel 2 only runs if a real Capsule
|
|
533
|
+
// is attached (Gene-only bundles are not supported by the A2A schema). Each
|
|
534
|
+
// channel's failure is isolated so a broken one cannot block the other.
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
function publishAssets(gene, capsule) {
|
|
537
|
+
const skillPromise = publishSkillChannel(gene);
|
|
538
|
+
const bundlePromise = capsule ? publishBundleChannel(gene, capsule) : Promise.resolve({ ok: false, skipped: 'no_capsule' });
|
|
539
|
+
return Promise.all([skillPromise, bundlePromise]).then(([skill, bundle]) => ({
|
|
540
|
+
skill_store: skill,
|
|
541
|
+
gep_bundle: bundle,
|
|
542
|
+
ok: Boolean((skill && skill.ok) || (bundle && bundle.ok)),
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function publishSkillChannel(gene) {
|
|
547
|
+
try {
|
|
548
|
+
const p = skillPublisher.publishSkillToHub(gene);
|
|
549
|
+
return Promise.resolve(p).catch((err) => ({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return Promise.resolve({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function publishBundleChannel(gene, capsule) {
|
|
556
|
+
const hubUrl = a2a.getHubUrl && a2a.getHubUrl();
|
|
557
|
+
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
|
|
558
|
+
let message;
|
|
559
|
+
try {
|
|
560
|
+
// buildPublishBundle mutates asset_id on the objects it receives, so
|
|
561
|
+
// clone first to avoid polluting the locally stored gene/capsule.
|
|
562
|
+
const geneClone = JSON.parse(JSON.stringify(gene));
|
|
563
|
+
const capsuleClone = JSON.parse(JSON.stringify(capsule));
|
|
564
|
+
message = a2a.buildPublishBundle({ gene: geneClone, capsule: capsuleClone });
|
|
565
|
+
} catch (err) {
|
|
566
|
+
return Promise.resolve({ ok: false, error: 'build_publish_bundle_failed: ' + (err && err.message ? err.message : String(err)) });
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const send = a2a.httpTransportSend(message, { hubUrl: hubUrl, timeoutMs: 15000 });
|
|
570
|
+
return Promise.resolve(send).catch((err) => ({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
571
|
+
} catch (err) {
|
|
572
|
+
return Promise.resolve({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
module.exports = {
|
|
577
|
+
SKILL2GEP_ID_PREFIX,
|
|
578
|
+
CAPSULE_ID_PREFIX,
|
|
579
|
+
RATIONALE_LINKS,
|
|
580
|
+
parseSkillMd,
|
|
581
|
+
synthesizeGene,
|
|
582
|
+
detectForgery,
|
|
583
|
+
assembleCapsule,
|
|
584
|
+
runOnSkillInvocation,
|
|
585
|
+
publishAssets,
|
|
586
|
+
publishSkillChannel,
|
|
587
|
+
publishBundleChannel,
|
|
588
|
+
logPath,
|
|
589
|
+
statePath,
|
|
590
|
+
DEFAULT_HOOK_TIMEOUT_MS,
|
|
591
|
+
};
|