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