@evomap/evolver 1.89.20 → 2.0.0-beta.0
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 +27 -564
- package/bin/evolver-llm-proxy.js +3 -0
- package/bin/evolver-mcp.js +2 -0
- package/bin/evolver-proxy.js +3 -0
- package/bin/evolver.js +4 -0
- package/index.js +1 -3586
- package/package.json +32 -62
- package/CONTRIBUTING.md +0 -19
- package/LICENSE +0 -641
- package/README.ja-JP.md +0 -521
- package/README.ko-KR.md +0 -520
- package/README.zh-CN.md +0 -531
- package/SKILL.md +0 -365
- package/assets/cover.png +0 -0
- package/assets/gep/genes.seed.json +0 -496
- package/conformance/savings-core/constants.json +0 -30
- package/conformance/savings-core/golden-vectors.json +0 -333
- package/scripts/a2a_export.js +0 -63
- package/scripts/a2a_ingest.js +0 -79
- package/scripts/a2a_promote.js +0 -118
- package/scripts/analyze_by_skill.js +0 -121
- package/scripts/build_binaries.js +0 -479
- package/scripts/check-changelog.js +0 -166
- package/scripts/extract_log.js +0 -85
- package/scripts/generate_history.js +0 -75
- package/scripts/gep_append_event.js +0 -96
- package/scripts/gep_personality_report.js +0 -234
- package/scripts/human_report.js +0 -147
- package/scripts/recall-verify-report.js +0 -234
- package/scripts/recover_loop.js +0 -61
- package/scripts/refresh_stars_badge.js +0 -168
- package/scripts/seed-merchants.js +0 -91
- package/scripts/skill2recipes.js +0 -118
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/skills/_meta/SKILL.md +0 -41
- package/skills/index.json +0 -14
- package/src/adapters/claudeCode.js +0 -194
- package/src/adapters/codex.js +0 -216
- package/src/adapters/cursor.js +0 -91
- package/src/adapters/hookAdapter.js +0 -469
- package/src/adapters/kiro.js +0 -195
- package/src/adapters/opencode.js +0 -326
- package/src/adapters/scripts/_lockPaths.js +0 -74
- package/src/adapters/scripts/_memoryFiltering.js +0 -35
- package/src/adapters/scripts/_runtimePaths.js +0 -440
- package/src/adapters/scripts/evolver-session-end.js +0 -321
- package/src/adapters/scripts/evolver-session-start.js +0 -587
- package/src/adapters/scripts/evolver-signal-detect.js +0 -98
- package/src/adapters/scripts/evolver-task-recall.js +0 -173
- package/src/atp/atpExecute.js +0 -283
- package/src/atp/atpTaskPickup.js +0 -233
- package/src/atp/autoBuyer.js +0 -382
- package/src/atp/autoDeliver.js +0 -215
- package/src/atp/cli.js +0 -354
- package/src/atp/cliAutobuyPrompt.js +0 -154
- package/src/atp/consumerAgent.js +0 -157
- package/src/atp/defaultHandler.js +0 -69
- package/src/atp/heartbeatSignalsHandler.js +0 -254
- package/src/atp/hubClient.js +0 -317
- package/src/atp/index.js +0 -38
- package/src/atp/merchantAgent.js +0 -118
- package/src/atp/protocol.js +0 -41
- package/src/atp/questionComposer.js +0 -133
- package/src/atp/serviceHelper.js +0 -92
- package/src/canary.js +0 -13
- package/src/config.js +0 -351
- package/src/evolve/guards.js +0 -1
- package/src/evolve/pipeline/collect.js +0 -1
- package/src/evolve/pipeline/dispatch.js +0 -1
- package/src/evolve/pipeline/enrich.js +0 -1
- package/src/evolve/pipeline/hub.js +0 -1
- package/src/evolve/pipeline/select.js +0 -1
- package/src/evolve/pipeline/signals.js +0 -1
- package/src/evolve/utils.js +0 -1
- package/src/evolve.js +0 -1
- package/src/experiment/agentRunner.js +0 -229
- package/src/experiment/cli.js +0 -159
- package/src/experiment/comparison.js +0 -233
- package/src/experiment/metrics.js +0 -75
- package/src/forceUpdate.js +0 -991
- package/src/gep/a2a.js +0 -173
- package/src/gep/a2aProtocol.js +0 -1
- package/src/gep/analyzer.js +0 -35
- package/src/gep/antiAbuseTelemetry.js +0 -1
- package/src/gep/assetCallLog.js +0 -197
- package/src/gep/assetStore.js +0 -723
- package/src/gep/assets.js +0 -36
- package/src/gep/autoDistillConv.js +0 -1
- package/src/gep/autoDistillLlm.js +0 -1
- package/src/gep/bridge.js +0 -138
- package/src/gep/candidateEval.js +0 -1
- package/src/gep/candidates.js +0 -1
- package/src/gep/claimNudge.js +0 -121
- package/src/gep/cliContracts.js +0 -1190
- package/src/gep/contentHash.js +0 -1
- package/src/gep/conversationDistiller.js +0 -1
- package/src/gep/conversationSniffer.js +0 -1
- package/src/gep/crypto.js +0 -1
- package/src/gep/curriculum.js +0 -1
- package/src/gep/deviceId.js +0 -1
- package/src/gep/directoryClient.js +0 -115
- package/src/gep/envFingerprint.js +0 -1
- package/src/gep/epigenetics.js +0 -1
- package/src/gep/execBridge.js +0 -1
- package/src/gep/executionTrace.js +0 -291
- package/src/gep/explore.js +0 -1
- package/src/gep/featureFlags.js +0 -121
- package/src/gep/gitOps.js +0 -265
- package/src/gep/hash.js +0 -1
- package/src/gep/hostErrorClassifier.js +0 -34
- package/src/gep/hubFetch.js +0 -1
- package/src/gep/hubReview.js +0 -1
- package/src/gep/hubSearch.js +0 -1
- package/src/gep/hubVerify.js +0 -1
- package/src/gep/idleScheduler.js +0 -400
- package/src/gep/issueReporter.js +0 -416
- package/src/gep/learningSignals.js +0 -1
- package/src/gep/llmReview.js +0 -92
- package/src/gep/localStateAwareness.js +0 -243
- package/src/gep/mailboxTransport.js +0 -119
- package/src/gep/memoryGraph.js +0 -1
- package/src/gep/memoryGraphAdapter.js +0 -1
- package/src/gep/mutation.js +0 -1
- package/src/gep/narrativeMemory.js +0 -1
- package/src/gep/oauthLogin.js +0 -181
- package/src/gep/openPRRegistry.js +0 -1
- package/src/gep/paths.js +0 -522
- package/src/gep/personality.js +0 -1
- package/src/gep/policyCheck.js +0 -1
- package/src/gep/portable.js +0 -103
- package/src/gep/privacyClient.js +0 -235
- package/src/gep/prompt.js +0 -1
- package/src/gep/questionGenerator.js +0 -518
- package/src/gep/recallInject.js +0 -1
- package/src/gep/recallVerifier.js +0 -1
- package/src/gep/reflection.js +0 -1
- package/src/gep/sanitize.js +0 -264
- package/src/gep/savingsCore.js +0 -1
- package/src/gep/schemas/capsule.js +0 -170
- package/src/gep/schemas/gene.js +0 -154
- package/src/gep/schemas/index.js +0 -8
- package/src/gep/schemas/protocol.js +0 -51
- package/src/gep/schemas/task.js +0 -74
- package/src/gep/selector.js +0 -1
- package/src/gep/selfPR.js +0 -469
- package/src/gep/signals.js +0 -776
- package/src/gep/skill2gep.js +0 -1056
- package/src/gep/skill2gepAudit.js +0 -303
- package/src/gep/skill2recipes.js +0 -511
- package/src/gep/skillDistiller.js +0 -1
- package/src/gep/skillPublisher.js +0 -358
- package/src/gep/solidify.js +0 -1
- package/src/gep/strategy.js +0 -1
- package/src/gep/taskReceiver.js +0 -575
- package/src/gep/tokenSavings.js +0 -1
- package/src/gep/trajectoryExport.js +0 -1
- package/src/gep/validationReport.js +0 -55
- package/src/gep/validator/index.js +0 -411
- package/src/gep/validator/reporter.js +0 -210
- package/src/gep/validator/sandboxExecutor.js +0 -480
- package/src/gep/validator/stakeBootstrap.js +0 -357
- package/src/gep/workspaceKeychain.js +0 -1
- package/src/ops/cleanup.js +0 -80
- package/src/ops/commentary.js +0 -60
- package/src/ops/health_check.js +0 -104
- package/src/ops/index.js +0 -11
- package/src/ops/innovation.js +0 -67
- package/src/ops/lifecycle.js +0 -798
- package/src/ops/self_repair.js +0 -76
- package/src/ops/skills_monitor.js +0 -147
- package/src/ops/trigger.js +0 -33
- package/src/proxy/clientSettings.js +0 -405
- package/src/proxy/envelope.js +0 -59
- package/src/proxy/extensions/dmHandler.js +0 -45
- package/src/proxy/extensions/sessionHandler.js +0 -141
- package/src/proxy/extensions/skillUpdater.js +0 -64
- package/src/proxy/extensions/traceControl.js +0 -1
- package/src/proxy/index.js +0 -1395
- package/src/proxy/inject.js +0 -1
- package/src/proxy/lifecycle/manager.js +0 -1568
- package/src/proxy/mailbox/state.js +0 -207
- package/src/proxy/mailbox/store.js +0 -590
- package/src/proxy/router/cache_passthrough.js +0 -26
- package/src/proxy/router/features.js +0 -84
- package/src/proxy/router/gemini_route.js +0 -154
- package/src/proxy/router/messages_route.js +0 -535
- package/src/proxy/router/model_router.js +0 -113
- package/src/proxy/router/models_route.js +0 -52
- package/src/proxy/router/ollama_route.js +0 -103
- package/src/proxy/router/responses_route.js +0 -170
- package/src/proxy/router/vertex_route.js +0 -110
- package/src/proxy/server/http.js +0 -363
- package/src/proxy/server/routes.js +0 -558
- package/src/proxy/server/settings.js +0 -115
- package/src/proxy/sync/engine.js +0 -179
- package/src/proxy/sync/inbound.js +0 -211
- package/src/proxy/sync/outbound.js +0 -320
- package/src/proxy/task/monitor.js +0 -131
- package/src/proxy/trace/extractor.js +0 -1
- package/src/proxy/trace/usage.js +0 -1
- package/src/webui/client/clientJs/assets.js +0 -111
- package/src/webui/client/clientJs/bootstrap.js +0 -92
- package/src/webui/client/clientJs/common.js +0 -77
- package/src/webui/client/clientJs/i18n.js +0 -366
- package/src/webui/client/clientJs/index.js +0 -35
- package/src/webui/client/clientJs/interactions.js +0 -351
- package/src/webui/client/clientJs/overview.js +0 -152
- package/src/webui/client/clientJs/personality.js +0 -285
- package/src/webui/client/clientJs/pipelines.js +0 -330
- package/src/webui/client/indexHtml.js +0 -221
- package/src/webui/client/static.js +0 -23
- package/src/webui/client/stylesCss.js +0 -639
- package/src/webui/client/vendor/README.md +0 -15
- package/src/webui/client/vendor/echarts.min.js +0 -45
- package/src/webui/index.js +0 -14
- package/src/webui/observer/assets.js +0 -146
- package/src/webui/observer/index.js +0 -37
- package/src/webui/observer/interactions.js +0 -127
- package/src/webui/observer/jsonl.js +0 -75
- package/src/webui/observer/paths.js +0 -46
- package/src/webui/observer/personality.js +0 -43
- package/src/webui/observer/pipelineEvents.js +0 -58
- package/src/webui/observer/redact.js +0 -63
- package/src/webui/observer/runs.js +0 -356
- package/src/webui/observer/safety.js +0 -57
- package/src/webui/observer/skills.js +0 -70
- package/src/webui/observer/status.js +0 -71
- package/src/webui/server/http.js +0 -138
- package/src/webui/server/routes.js +0 -41
package/src/gep/skill2gep.js
DELETED
|
@@ -1,1056 +0,0 @@
|
|
|
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
|
-
const audit = require('./skill2gepAudit');
|
|
33
|
-
|
|
34
|
-
const SKILL2GEP_ID_PREFIX = 'gene_s2g_';
|
|
35
|
-
|
|
36
|
-
// Max strategy steps kept on a distilled Gene. The old value (10) silently
|
|
37
|
-
// truncated multi-section Skills, dropping the *governance* tail
|
|
38
|
-
// (candidate-gating, Human Gate, Output Contract, rollback) that lives at the
|
|
39
|
-
// end of a well-formed SKILL.md. extractSteps emits each list item flatly, so
|
|
40
|
-
// a rich Skill (workflow + governance sections) yields ~25-27 short one-line
|
|
41
|
-
// steps; the cap must clear that to keep the tail. 28 covers a well-formed
|
|
42
|
-
// SKILL.md while staying compact (short one-liners, far below a full Skill's
|
|
43
|
-
// token weight). Genuinely longer Skills are still bounded here.
|
|
44
|
-
const MAX_STRATEGY_STEPS = 28;
|
|
45
|
-
const CAPSULE_ID_PREFIX = 'cap_s2g_';
|
|
46
|
-
const LOG_FILE = 'skill2gep_log.jsonl';
|
|
47
|
-
const STATE_FILE = 'skill2gep_state.json';
|
|
48
|
-
const DEFAULT_HOOK_TIMEOUT_MS = 25000;
|
|
49
|
-
|
|
50
|
-
// Paper + docs we cite in the rationale field so agents can explain to users
|
|
51
|
-
// why we ship Genes/Capsules in addition to the human-facing Skill.
|
|
52
|
-
// NOTE: The paper validates Gene as a control-dense interface on 45 scientific
|
|
53
|
-
// code-solving scenarios with Gemini 3.1 Pro/Flash Lite. Generalization to other
|
|
54
|
-
// agent domains (web ops, long tool chains, multi-agent negotiation, etc.) is an
|
|
55
|
-
// explicit assumption of this tool, not a proven result. The rationale string
|
|
56
|
-
// we emit reflects this.
|
|
57
|
-
const RATIONALE_LINKS = {
|
|
58
|
-
paper: 'Wang, Ren, Zhang. From Procedural Skills to Strategy Genes. arXiv:2604.15097',
|
|
59
|
-
protocol: 'https://evomap.ai/wiki/16-gep-protocol',
|
|
60
|
-
skill_store: 'https://evomap.ai/wiki/31-skill-store',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const RATIONALE_TEXT = ''
|
|
64
|
-
+ 'Emitted both the human-facing Skill and the machine-facing GEP asset(s). '
|
|
65
|
-
+ 'In the paper\'s domain (45 scientific code-solving scenarios, Gemini 3.1 '
|
|
66
|
-
+ 'Pro/Flash Lite; ' + 'Wang, Ren, Zhang, arXiv:2604.15097'
|
|
67
|
-
+ '), Gene-as-control-interface outperforms procedural SKILL.md. '
|
|
68
|
-
+ 'Generalization to other domains is an assumption of this tool, not a '
|
|
69
|
-
+ 'proven result; outcome quality depends on the source Skill and on real '
|
|
70
|
-
+ 'execution evidence. See ' + 'https://evomap.ai/wiki/16-gep-protocol'
|
|
71
|
-
+ ' for the protocol.';
|
|
72
|
-
|
|
73
|
-
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
|
|
74
|
-
|
|
75
|
-
function readJsonSafe(p, fallback) {
|
|
76
|
-
try {
|
|
77
|
-
if (!fs.existsSync(p)) return fallback;
|
|
78
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
79
|
-
if (!raw.trim()) return fallback;
|
|
80
|
-
return JSON.parse(raw);
|
|
81
|
-
} catch (_) { return fallback; }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function appendJsonl(p, obj) {
|
|
85
|
-
ensureDir(path.dirname(p));
|
|
86
|
-
fs.appendFileSync(p, JSON.stringify(obj) + '\n', 'utf8');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function logPath() { return path.join(paths.getMemoryDir(), LOG_FILE); }
|
|
90
|
-
function statePath() { return path.join(paths.getMemoryDir(), STATE_FILE); }
|
|
91
|
-
|
|
92
|
-
function readState() { return readJsonSafe(statePath(), { seen: {} }); }
|
|
93
|
-
function writeState(s) {
|
|
94
|
-
ensureDir(path.dirname(statePath()));
|
|
95
|
-
const tmp = statePath() + '.tmp';
|
|
96
|
-
fs.writeFileSync(tmp, JSON.stringify(s, null, 2) + '\n', 'utf8');
|
|
97
|
-
fs.renameSync(tmp, statePath());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function slugify(s) {
|
|
101
|
-
return String(s || '')
|
|
102
|
-
.toLowerCase()
|
|
103
|
-
.replace(/[^a-z0-9]+/g, '_')
|
|
104
|
-
.replace(/^_+|_+$/g, '')
|
|
105
|
-
.slice(0, 60);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function shortHash(s) {
|
|
109
|
-
return crypto.createHash('sha256').update(String(s || '')).digest('hex').slice(0, 10);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeCmd(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
|
|
113
|
-
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// Parse a procedural SKILL.md / markdown workflow into structured sections.
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
function parseSkillMd(skillMd) {
|
|
118
|
-
const text = String(skillMd || '');
|
|
119
|
-
|
|
120
|
-
let frontmatter = {};
|
|
121
|
-
const fmMatch = text.match(/^---\n([\s\S]*?)\n---\n/);
|
|
122
|
-
let body = text;
|
|
123
|
-
if (fmMatch) {
|
|
124
|
-
fmMatch[1].split(/\n/).forEach((line) => {
|
|
125
|
-
const kv = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
126
|
-
if (kv) frontmatter[kv[1].trim().toLowerCase()] = kv[2].trim();
|
|
127
|
-
});
|
|
128
|
-
body = text.slice(fmMatch[0].length);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const sections = {};
|
|
132
|
-
let currentKey = '_preamble';
|
|
133
|
-
sections[currentKey] = [];
|
|
134
|
-
body.split(/\n/).forEach((line) => {
|
|
135
|
-
const hdr = line.match(/^##+\s+(.+?)\s*$/);
|
|
136
|
-
if (hdr) {
|
|
137
|
-
currentKey = hdr[1].toLowerCase().trim();
|
|
138
|
-
sections[currentKey] = [];
|
|
139
|
-
} else {
|
|
140
|
-
sections[currentKey].push(line);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
Object.keys(sections).forEach((k) => { sections[k] = sections[k].join('\n').trim(); });
|
|
144
|
-
|
|
145
|
-
// Return the FIRST matching section (kept for signals, which wants one block).
|
|
146
|
-
function pickSection(keywords) {
|
|
147
|
-
for (const kw of keywords) {
|
|
148
|
-
for (const k of Object.keys(sections)) {
|
|
149
|
-
if (k.indexOf(kw) !== -1) return sections[k];
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return '';
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Return ALL matching sections concatenated, in document order. A SKILL.md
|
|
156
|
-
// often spreads positive steps across several headed sections ("Quick
|
|
157
|
-
// Workflow", "Human Gate Defaults", "Output Contract"); picking only the
|
|
158
|
-
// first dropped the governance tail. Each section's title is preserved as a
|
|
159
|
-
// step-context line so a trailing "## Human Gate" still contributes its
|
|
160
|
-
// bullets. De-duplicated by section key.
|
|
161
|
-
function pickSectionsAll(keywords) {
|
|
162
|
-
const seen = new Set();
|
|
163
|
-
const out = [];
|
|
164
|
-
for (const k of Object.keys(sections)) {
|
|
165
|
-
if (keywords.some((kw) => k.indexOf(kw) !== -1) && !seen.has(k)) {
|
|
166
|
-
seen.add(k);
|
|
167
|
-
out.push(sections[k]);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return out.join('\n');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Extract ordered steps from a markdown block: every list item becomes its
|
|
174
|
-
// own step, in document order. This is the pre-PR flat behaviour, kept
|
|
175
|
-
// deliberately simple — an earlier version folded indented sub-bullets into
|
|
176
|
-
// their parent step to look tidier, but that indentation logic grew a long
|
|
177
|
-
// tail of edge cases (section-trim interaction, length-filtered parents,
|
|
178
|
-
// cross-section indentation). Folding was only cosmetic; flat extraction
|
|
179
|
-
// preserves the same governance tail with no indentation reasoning at all.
|
|
180
|
-
// opts.minLen / opts.maxLen bound each item (defaults 5..300, matching the
|
|
181
|
-
// original strategy/avoid gate). Preconditions pass {minLen: 1,
|
|
182
|
-
// maxLen: Infinity} so short prerequisites like "Git"/"npm" survive.
|
|
183
|
-
function extractSteps(block, opts) {
|
|
184
|
-
const minLen = opts && typeof opts.minLen === 'number' ? opts.minLen : 5;
|
|
185
|
-
const maxLen = opts && typeof opts.maxLen === 'number' ? opts.maxLen : 300;
|
|
186
|
-
const steps = [];
|
|
187
|
-
for (const line of String(block || '').split(/\n/)) {
|
|
188
|
-
const m = line.match(/^\s*(?:\d+\.|[-*])\s+(.+?)\s*$/);
|
|
189
|
-
if (!m) continue;
|
|
190
|
-
const txt = m[1].trim();
|
|
191
|
-
if (txt.length >= minLen && txt.length <= maxLen) steps.push(txt);
|
|
192
|
-
}
|
|
193
|
-
return steps;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const signals = [];
|
|
197
|
-
// Section keywords are matched against lower-cased headings. A SKILL.md may
|
|
198
|
-
// be authored in Chinese (e.g. game-* skills use "## 何时使用" / "## 触发条件"),
|
|
199
|
-
// whose heading key never contains an English token, so the CJK synonyms
|
|
200
|
-
// below are required for those skills to contribute signals/strategy/avoid
|
|
201
|
-
// at all — without them the distiller silently falls back to a thin gene.
|
|
202
|
-
// NOTE: this only fixes *section matching*. The signal tokenizer below still
|
|
203
|
-
// keeps ASCII [a-z0-9_] only, so signals for a Chinese skill come from its
|
|
204
|
-
// (English) frontmatter description, not from CJK body words. CJK signal
|
|
205
|
-
// tokenization needs a word segmenter and is intentionally out of scope here.
|
|
206
|
-
const signalSource = (frontmatter.description || '') + '\n' + pickSection([
|
|
207
|
-
'trigger', 'when to use', 'when', 'use when', 'scenario',
|
|
208
|
-
'何时使用', '什么时候使用', '触发条件', '触发', '使用场景', '核心目标', '适用',
|
|
209
|
-
]);
|
|
210
|
-
signalSource.split(/[`,.\n]/).forEach((tok) => {
|
|
211
|
-
const s = tok.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
|
212
|
-
if (s.length >= 3 && s.length <= 40 && /[a-z]/.test(s) && signals.indexOf(s) === -1 && !/^\d+$/.test(s)) {
|
|
213
|
-
signals.push(s);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Strategy spans the workflow AND the governance tail (Human Gate, Output
|
|
218
|
-
// Contract) — concatenate all matching sections so the candidate/gate/rollback
|
|
219
|
-
// discipline survives, and fold nested sub-bullets into their parent step.
|
|
220
|
-
const strategyBlock = pickSectionsAll([
|
|
221
|
-
'workflow', 'strategy', 'steps', 'procedure', 'quick start', 'how to',
|
|
222
|
-
'human gate', 'output contract', 'release', 'rollback', 'promotion',
|
|
223
|
-
// CJK synonyms: positive workflow + governance-tail headings.
|
|
224
|
-
'工作流', '流程', '步骤', '核心方法', '方法', '快速规则', '规则',
|
|
225
|
-
'输出门', '输出门槛', '人工确认', '人工门', '回滚', '发布', '晋级',
|
|
226
|
-
]);
|
|
227
|
-
const strategy = extractSteps(strategyBlock);
|
|
228
|
-
|
|
229
|
-
const avoidBlock = pickSectionsAll([
|
|
230
|
-
'avoid', 'pitfall', 'anti-pattern', 'common mistake', 'do not', 'forbidden', "don't",
|
|
231
|
-
// CJK synonyms: anti-pattern / "do not" headings.
|
|
232
|
-
'不要做', '不要', '常见错误', '避免', '陷阱', '禁止',
|
|
233
|
-
]);
|
|
234
|
-
const avoid = extractSteps(avoidBlock);
|
|
235
|
-
|
|
236
|
-
const validation = [];
|
|
237
|
-
const valBlock = pickSection(['validation', 'test', 'verify', 'check', '校验', '验证', '测试', '检查']);
|
|
238
|
-
const fenceRe = /```(?:bash|sh|shell)?\s*\n([\s\S]*?)\n```/g;
|
|
239
|
-
let fm;
|
|
240
|
-
while ((fm = fenceRe.exec(valBlock)) !== null) {
|
|
241
|
-
fm[1].split(/\n/).forEach((ln) => {
|
|
242
|
-
const t = ln.trim();
|
|
243
|
-
if (t && !t.startsWith('#') && t.length <= 300) validation.push(t);
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Preconditions keep the pre-PR behaviour: no length gate, no folding, so
|
|
248
|
-
// short items like "Git"/"npm" survive and preconditions_extracted is stable.
|
|
249
|
-
const preBlock = pickSection(['precondition', 'requirement', 'prerequisite', '前置条件', '前置', '先决条件', '要求']);
|
|
250
|
-
const preconditions = extractSteps(preBlock, { minLen: 1, maxLen: Infinity });
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
frontmatter: frontmatter,
|
|
254
|
-
sections: sections,
|
|
255
|
-
name: frontmatter.name || (sections['_preamble'] || '').split(/\n/)[0].replace(/^#+\s*/, '').trim(),
|
|
256
|
-
description: frontmatter.description || '',
|
|
257
|
-
signals_match: signals.slice(0, 8),
|
|
258
|
-
strategy: strategy.slice(0, MAX_STRATEGY_STEPS),
|
|
259
|
-
avoid: avoid.slice(0, 5),
|
|
260
|
-
validation: validation.slice(0, 5),
|
|
261
|
-
preconditions: preconditions.slice(0, 4),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// Provenance classification (the central finding of TaskGenome Bench, §3.1):
|
|
267
|
-
// a Gene's value depends on WHERE it came from, not on being short.
|
|
268
|
-
//
|
|
269
|
-
// evolved -- distilled from a real solve -> fail -> mutate -> pass
|
|
270
|
-
// trajectory. The corrective insight that flipped the outcome
|
|
271
|
-
// is the high-value payload. These beat Skills (+8.7..+15.5pp).
|
|
272
|
-
// distilled -- transcribed from reference/teacher text with no real failing
|
|
273
|
-
// trajectory to learn from. The report shows these tend to be
|
|
274
|
-
// WORSE than Skills (-3.2..-11.2pp), so we flag/downgrade them.
|
|
275
|
-
// manual -- pure SKILL.md transcription, no execution evidence at all.
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
function classifyProvenance(execution) {
|
|
278
|
-
const ex = execution || {};
|
|
279
|
-
const rollouts = Array.isArray(ex.rollouts) ? ex.rollouts : [];
|
|
280
|
-
const mutationLog = Array.isArray(ex.mutation_log) ? ex.mutation_log : [];
|
|
281
|
-
const status = ex.status ? String(ex.status) : null;
|
|
282
|
-
const blast = ex.blast_radius || null;
|
|
283
|
-
const hasBlast = blast && (Number(blast.files || 0) > 0 || Number(blast.lines || 0) > 0);
|
|
284
|
-
|
|
285
|
-
const failedRollouts = rollouts.filter((r) => r && String(r.status) === 'failed').length;
|
|
286
|
-
const passedRollouts = rollouts.some((r) => r && String(r.status) === 'success');
|
|
287
|
-
const overcameFailure = mutationLog.length > 0 || (failedRollouts > 0 && (passedRollouts || status === 'success'));
|
|
288
|
-
|
|
289
|
-
if (status === 'success' && hasBlast && overcameFailure) return 'evolved';
|
|
290
|
-
// Anything carrying real execution evidence (a status, a rollout, or an
|
|
291
|
-
// overcome-failure log) but not meeting the evolved bar is "distilled" -- it
|
|
292
|
-
// has evidence, just not a verified fail->pass-with-blast trajectory. Only a
|
|
293
|
-
// run with NO evidence at all is "manual" (per docs/skill2gep.md). A success
|
|
294
|
-
// with mutation_log but zero blast radius must therefore be distilled, not
|
|
295
|
-
// manual.
|
|
296
|
-
if ((ex.reference_distilled === true) || status || rollouts.length > 0 || mutationLog.length > 0) {
|
|
297
|
-
return 'distilled';
|
|
298
|
-
}
|
|
299
|
-
return 'manual';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Build the corrective-insight strategy for an evolved Gene. The insight that
|
|
303
|
-
// flipped fail -> pass goes FIRST (the case-study shape), then any
|
|
304
|
-
// LLM-distilled steps the host supplied, then the Skill's own workflow steps.
|
|
305
|
-
function buildEvolvedStrategy(parsed, execution) {
|
|
306
|
-
const strategy = [];
|
|
307
|
-
const insight = execution && execution.corrective_insight
|
|
308
|
-
? String(execution.corrective_insight).trim()
|
|
309
|
-
: '';
|
|
310
|
-
if (insight && insight.length >= 5) {
|
|
311
|
-
strategy.push(insight.length <= 300 ? insight : insight.slice(0, 297) + '...');
|
|
312
|
-
}
|
|
313
|
-
const distilled = Array.isArray(execution && execution.distilled_strategy) ? execution.distilled_strategy : [];
|
|
314
|
-
distilled.forEach((s) => {
|
|
315
|
-
const t = String(s || '').trim();
|
|
316
|
-
if (t.length >= 5 && strategy.indexOf(t) === -1) strategy.push(t.length <= 300 ? t : t.slice(0, 297) + '...');
|
|
317
|
-
});
|
|
318
|
-
(parsed.strategy || []).forEach((s) => { if (strategy.indexOf(s) === -1) strategy.push(s); });
|
|
319
|
-
return strategy;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Turn the error categories the trajectory overcame into verifiable
|
|
323
|
-
// preconditions ("a prior attempt failed with X; confirm it is handled").
|
|
324
|
-
function preconditionsFromErrors(execution) {
|
|
325
|
-
const mutationLog = Array.isArray(execution && execution.mutation_log) ? execution.mutation_log : [];
|
|
326
|
-
const out = [];
|
|
327
|
-
const seen = new Set();
|
|
328
|
-
for (const err of mutationLog) {
|
|
329
|
-
const e = String(err || '').trim();
|
|
330
|
-
if (!e || seen.has(e)) continue;
|
|
331
|
-
seen.add(e);
|
|
332
|
-
const human = e.replace(/_/g, ' ');
|
|
333
|
-
out.push('A prior attempt failed with "' + human + '"; verify this condition is handled before trusting the approach.');
|
|
334
|
-
if (out.length >= 4) break;
|
|
335
|
-
}
|
|
336
|
-
return out;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Quality score in [0,1]. Evolved trajectories with a recorded corrective
|
|
340
|
-
// insight score highest; pure transcription with no evidence scores lowest.
|
|
341
|
-
function computeQualityScore(source, parsed, execution) {
|
|
342
|
-
const ex = execution || {};
|
|
343
|
-
let score;
|
|
344
|
-
if (source === 'evolved') {
|
|
345
|
-
score = 0.7;
|
|
346
|
-
if (ex.corrective_insight && String(ex.corrective_insight).trim().length >= 5) score += 0.15;
|
|
347
|
-
const depth = Array.isArray(ex.mutation_log) ? ex.mutation_log.length
|
|
348
|
-
: (Array.isArray(ex.rollouts) ? ex.rollouts.length - 1 : 0);
|
|
349
|
-
if (depth >= 1) score += Math.min(0.15, depth * 0.05);
|
|
350
|
-
} else if (source === 'distilled') {
|
|
351
|
-
score = 0.4;
|
|
352
|
-
} else {
|
|
353
|
-
score = 0.3;
|
|
354
|
-
}
|
|
355
|
-
const strategySteps = (parsed.strategy || []).length;
|
|
356
|
-
if (strategySteps >= 4) score += 0.05;
|
|
357
|
-
if ((parsed.avoid || []).length >= 1) score += 0.05;
|
|
358
|
-
return Math.max(0, Math.min(1, Number(score.toFixed(3))));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
|
-
// Synthesize a draft Gene from parsed Skill + execution trace.
|
|
363
|
-
//
|
|
364
|
-
// The strategy/preconditions content depends on provenance:
|
|
365
|
-
// - evolved -> corrective insight first, overcome-errors -> preconditions.
|
|
366
|
-
// - otherwise -> Skill transcription (legacy behavior), tagged so consumers
|
|
367
|
-
// know it was not learned from a real run.
|
|
368
|
-
//
|
|
369
|
-
// Validation is delegated to skillDistiller.validateSynthesizedGene() so that
|
|
370
|
-
// we reuse the sanitization, ID-rewrite, forbidden-path, and validation-cmd
|
|
371
|
-
// policy rules already hardened there.
|
|
372
|
-
// ---------------------------------------------------------------------------
|
|
373
|
-
function synthesizeGene(parsed, execution, opts) {
|
|
374
|
-
execution = execution || {};
|
|
375
|
-
opts = opts || {};
|
|
376
|
-
const traceSignals = Array.isArray(execution.signals) ? execution.signals : [];
|
|
377
|
-
const mergedSignals = Array.from(new Set([].concat(parsed.signals_match || [], traceSignals)));
|
|
378
|
-
|
|
379
|
-
const source = classifyProvenance(execution);
|
|
380
|
-
|
|
381
|
-
// Strategy source depends on provenance. For an evolved trajectory the
|
|
382
|
-
// corrective insight leads (this is what beats a Skill); otherwise we
|
|
383
|
-
// transcribe the Skill's own workflow, tagged so consumers know it was not
|
|
384
|
-
// learned from a real run.
|
|
385
|
-
let strategy;
|
|
386
|
-
if (source === 'evolved') {
|
|
387
|
-
strategy = buildEvolvedStrategy(parsed, execution);
|
|
388
|
-
} else {
|
|
389
|
-
strategy = [];
|
|
390
|
-
(parsed.strategy || []).forEach((s) => strategy.push(s));
|
|
391
|
-
}
|
|
392
|
-
if (strategy.length < 3) {
|
|
393
|
-
strategy.push('Identify the dominant trigger signals from the Skill description.');
|
|
394
|
-
strategy.push('Apply the smallest targeted change that satisfies the Skill workflow.');
|
|
395
|
-
strategy.push('Run the Skill validation commands and abort if any fails.');
|
|
396
|
-
}
|
|
397
|
-
const avoid = Array.isArray(parsed.avoid) ? parsed.avoid.slice(0, 5) : [];
|
|
398
|
-
|
|
399
|
-
// Preconditions: for evolved Genes, the error categories the trajectory had
|
|
400
|
-
// to overcome become verifiable preconditions; the Skill's declared
|
|
401
|
-
// preconditions (and any host-distilled ones) are appended.
|
|
402
|
-
let preconditions;
|
|
403
|
-
if (source === 'evolved') {
|
|
404
|
-
preconditions = preconditionsFromErrors(execution)
|
|
405
|
-
.concat(Array.isArray(execution.distilled_preconditions) ? execution.distilled_preconditions.map(String) : [])
|
|
406
|
-
.concat(parsed.preconditions || []);
|
|
407
|
-
} else {
|
|
408
|
-
preconditions = (parsed.preconditions && parsed.preconditions.length > 0)
|
|
409
|
-
? parsed.preconditions.slice()
|
|
410
|
-
: ['Skill ' + (parsed.name || 'unknown') + ' has just been executed locally'];
|
|
411
|
-
}
|
|
412
|
-
if (preconditions.length === 0) {
|
|
413
|
-
preconditions = ['Skill ' + (parsed.name || 'unknown') + ' has just been executed locally'];
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Filter validation commands through the same allow-list that
|
|
417
|
-
// validateSynthesizedGene will later apply (node/npm/npx only). Per the
|
|
418
|
-
// Gene-Bench DISTILL contract ("validation: [] only; do not add bogus
|
|
419
|
-
// console-log validations"), we no longer inject a near-trivial
|
|
420
|
-
// 'node --version' when nothing runnable is found -- an empty validation
|
|
421
|
-
// list is the correct outcome for a Gene asset.
|
|
422
|
-
//
|
|
423
|
-
// strict mode is a different consumer: skill2recipes calls us with
|
|
424
|
-
// strict=true because a recipe STEP must carry a real, runnable check (its
|
|
425
|
-
// verify stage executes the commands). An empty validation there would
|
|
426
|
-
// verify nothing, so strict still rejects it.
|
|
427
|
-
const policyCheck = require('./policyCheck');
|
|
428
|
-
const rawValidations = Array.isArray(parsed.validation) ? parsed.validation : [];
|
|
429
|
-
const allowedValidations = rawValidations
|
|
430
|
-
.map((v) => String(v || '').trim())
|
|
431
|
-
.filter((v) => v && policyCheck.isValidationCommandAllowed(v));
|
|
432
|
-
const validation = allowedValidations;
|
|
433
|
-
if (Boolean(opts.strict) && validation.length === 0) {
|
|
434
|
-
return {
|
|
435
|
-
valid: false,
|
|
436
|
-
errors: [
|
|
437
|
-
'strict mode: no allowed validation commands found in the Skill. '
|
|
438
|
-
+ 'GEP validation only permits "node "/"npm "/"npx " prefixes. '
|
|
439
|
-
+ 'Rewrite the Skill\'s validation section with those, or drop --strict.',
|
|
440
|
-
],
|
|
441
|
-
gene: null,
|
|
442
|
-
source: source,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Quality: a coarse score (used by the quality gate to downgrade thin
|
|
447
|
-
// distilled Genes) plus descriptive heuristics for reviewers.
|
|
448
|
-
const qualityScore = computeQualityScore(source, parsed, execution);
|
|
449
|
-
const qualityHeuristics = {
|
|
450
|
-
strategy_steps: (parsed.strategy || []).length,
|
|
451
|
-
avoid_count: (parsed.avoid || []).length,
|
|
452
|
-
validation_declared_count: rawValidations.length,
|
|
453
|
-
validation_runnable_count: allowedValidations.length,
|
|
454
|
-
signals_extracted: (parsed.signals_match || []).length,
|
|
455
|
-
preconditions_extracted: (parsed.preconditions || []).length,
|
|
456
|
-
trajectory_depth: Array.isArray(execution.mutation_log) ? execution.mutation_log.length
|
|
457
|
-
: (Array.isArray(execution.rollouts) ? Math.max(0, execution.rollouts.length - 1) : 0),
|
|
458
|
-
has_corrective_insight: Boolean(execution.corrective_insight
|
|
459
|
-
&& String(execution.corrective_insight).trim().length >= 5),
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const skillSlug = slugify(parsed.name || opts.skillName || 'skill');
|
|
463
|
-
let draft = {
|
|
464
|
-
type: 'Gene',
|
|
465
|
-
id: SKILL2GEP_ID_PREFIX + skillSlug,
|
|
466
|
-
summary: (parsed.description || strategy[0] || 'Reusable strategy distilled from Skill').slice(0, 200),
|
|
467
|
-
category: inferCategory(mergedSignals, parsed.description),
|
|
468
|
-
signals_match: mergedSignals.slice(0, 8),
|
|
469
|
-
preconditions: preconditions.slice(0, 6),
|
|
470
|
-
strategy: strategy.slice(0, MAX_STRATEGY_STEPS),
|
|
471
|
-
avoid: avoid,
|
|
472
|
-
constraints: {
|
|
473
|
-
max_files: opts.maxFiles || skillDistiller.DISTILLED_MAX_FILES,
|
|
474
|
-
forbidden_paths: ['.git', 'node_modules'],
|
|
475
|
-
},
|
|
476
|
-
validation: validation,
|
|
477
|
-
schema_version: '1.6.0',
|
|
478
|
-
_source: {
|
|
479
|
-
kind: 'skill2gep',
|
|
480
|
-
generation_source: source,
|
|
481
|
-
skill_name: parsed.name || null,
|
|
482
|
-
skill_platform: opts.platform || null,
|
|
483
|
-
skill_hash: opts.skillHash ? opts.skillHash : null,
|
|
484
|
-
rationale_paper: RATIONALE_LINKS.paper,
|
|
485
|
-
paper_scope: 'code-science (arXiv:2604.15097, 45 tasks, Gemini 3.1 Pro/Flash Lite)',
|
|
486
|
-
claims_outside_scope: 'assumption',
|
|
487
|
-
quality_score: qualityScore,
|
|
488
|
-
overcame_errors: Array.isArray(execution.mutation_log) ? execution.mutation_log.slice(0, 8) : [],
|
|
489
|
-
quality_heuristics: qualityHeuristics,
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
// Mechanical leakage audit (Gene-Bench Stage-3): strip any hard literal that
|
|
494
|
-
// appears only in the run's hidden text (final solution / verifier feedback)
|
|
495
|
-
// and not in the public SKILL.md. Run BEFORE validateSynthesizedGene so the
|
|
496
|
-
// sanitized payload is what gets ID-rewritten and persisted.
|
|
497
|
-
let auditInfo = { leaks_found_count: 0, redacted: false };
|
|
498
|
-
if (opts.skillMd) {
|
|
499
|
-
const privateVocab = audit.buildPrivateVocab(opts.skillMd, execution);
|
|
500
|
-
const leaks = audit.findLeakage(draft, privateVocab);
|
|
501
|
-
if (leaks.length > 0) {
|
|
502
|
-
draft = audit.redactPrivateLiterals(draft, privateVocab);
|
|
503
|
-
const residual = audit.findLeakage(draft, privateVocab);
|
|
504
|
-
// Record only counts/locations, never the private literals themselves --
|
|
505
|
-
// storing the leaked token verbatim in the published asset would defeat
|
|
506
|
-
// the audit.
|
|
507
|
-
auditInfo = {
|
|
508
|
-
leaks_found_count: leaks.length,
|
|
509
|
-
leak_locations: Array.from(new Set(leaks.map((l) => l.location))),
|
|
510
|
-
redacted: true,
|
|
511
|
-
residual_leak_count: residual.length,
|
|
512
|
-
};
|
|
513
|
-
draft._source.leakage_audit = auditInfo;
|
|
514
|
-
if (opts.strict && residual.length > 0) {
|
|
515
|
-
return {
|
|
516
|
-
valid: false,
|
|
517
|
-
errors: ['strict mode: leakage audit could not remove ' + residual.length
|
|
518
|
-
+ ' private literal(s) at: ' + Array.from(new Set(residual.map((l) => l.location))).join(', ')],
|
|
519
|
-
gene: null,
|
|
520
|
-
source: source,
|
|
521
|
-
quality_score: qualityScore,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
// The audit may have dropped validation commands that carried a private
|
|
525
|
-
// literal. Re-assert strict mode's runnable-validation requirement here,
|
|
526
|
-
// since the earlier check ran before redaction.
|
|
527
|
-
if (opts.strict && (!Array.isArray(draft.validation) || draft.validation.length === 0)) {
|
|
528
|
-
return {
|
|
529
|
-
valid: false,
|
|
530
|
-
errors: ['strict mode: all runnable validation commands were dropped by the '
|
|
531
|
-
+ 'leakage audit (they contained private literals), leaving no verifiable check.'],
|
|
532
|
-
gene: null,
|
|
533
|
-
source: source,
|
|
534
|
-
quality_score: qualityScore,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const assetsDir = paths.getGepAssetsDir();
|
|
541
|
-
const existingGenesJson = readJsonSafe(path.join(assetsDir, 'genes.json'), { genes: [] });
|
|
542
|
-
const existingGenes = Array.isArray(existingGenesJson.genes) ? existingGenesJson.genes : [];
|
|
543
|
-
const result = skillDistiller.validateSynthesizedGene(draft, existingGenes);
|
|
544
|
-
// Surface provenance + quality so the caller's quality gate can act on it.
|
|
545
|
-
result.source = source;
|
|
546
|
-
result.quality_score = qualityScore;
|
|
547
|
-
result.audit = auditInfo;
|
|
548
|
-
return result;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function inferCategory(signals, description) {
|
|
552
|
-
const hay = ((description || '') + ' ' + (signals || []).join(' ')).toLowerCase();
|
|
553
|
-
// Priority repair -> innovate -> optimize, mirroring the sibling
|
|
554
|
-
// inferCategoryFromSignals() in skillDistiller.js / solidify.js.
|
|
555
|
-
//
|
|
556
|
-
// REPAIR set uses SUBSTRING matching (no \b): it must catch both inflected
|
|
557
|
-
// forms ("errors", "fixed", "crashes") and the project's underscore signal
|
|
558
|
-
// format ("log_error", "test_failure"), which a \b-anchored regex breaks
|
|
559
|
-
// (\b treats `_` as a word char, so "error" inside "log_error" has no
|
|
560
|
-
// boundary). Changes vs. the pre-PR original:
|
|
561
|
-
// - repair: removed "rollback"/"guard" — cross-cutting safety words common
|
|
562
|
-
// in *optimize* skills (e.g. paranoia-ai-system-evolver lists "rollback"
|
|
563
|
-
// in its safe-change method) that must not by themselves force repair.
|
|
564
|
-
// - innovate: "add" is matched with a \b word boundary so it catches the
|
|
565
|
-
// verb ("add a dashboard") without false-positives on address/additional/
|
|
566
|
-
// padding (the pre-PR bare-substring "add" matched all of those). The
|
|
567
|
-
// innovate set only reads natural-language description, so \b is safe here.
|
|
568
|
-
if (/error|fail|bug|crash|broken|incident|regress|debug|repair|fix/.test(hay)) {
|
|
569
|
-
return 'repair';
|
|
570
|
-
}
|
|
571
|
-
if (/feature|\badd\b|implement|new capability|capability|innovate|greenfield|prototype/.test(hay)) {
|
|
572
|
-
return 'innovate';
|
|
573
|
-
}
|
|
574
|
-
return 'optimize';
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
// LLM distillation = the host agent.
|
|
579
|
-
//
|
|
580
|
-
// The evolver engine has no in-process LLM client and never spawns one. The
|
|
581
|
-
// "LLM" IS the host agent (Claude Code / Cursor / Codex) that just ran the
|
|
582
|
-
// Skill -- it already has the full execution in context. So this stage does
|
|
583
|
-
// not call out anywhere: it simply consumes the distillation the host agent
|
|
584
|
-
// provides inline on opts.execution (docs/skill2gep.md):
|
|
585
|
-
//
|
|
586
|
-
// - execution.corrective_insight : the single fix that flipped fail -> pass
|
|
587
|
-
// (becomes strategy[0]).
|
|
588
|
-
// - execution.distilled_payload : optional { corrective_insight, strategy,
|
|
589
|
-
// preconditions } the host already wrote.
|
|
590
|
-
//
|
|
591
|
-
// If the host supplied neither, synthesizeGene falls back to the mechanical
|
|
592
|
-
// corrective distillation. Zero network, zero subprocess, in-budget.
|
|
593
|
-
// ---------------------------------------------------------------------------
|
|
594
|
-
function _hostDistilledPayload(execution) {
|
|
595
|
-
const p = execution && execution.distilled_payload;
|
|
596
|
-
if (!p || typeof p !== 'object') return null;
|
|
597
|
-
const out = {};
|
|
598
|
-
if (Array.isArray(p.strategy) && p.strategy.length) out.strategy = p.strategy.map(String);
|
|
599
|
-
if (Array.isArray(p.preconditions)) out.preconditions = p.preconditions.map(String);
|
|
600
|
-
if (typeof p.corrective_insight === 'string') out.corrective_insight = p.corrective_insight;
|
|
601
|
-
return Object.keys(out).length ? out : null;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function distillWithLLM(parsed, execution, opts) { // eslint-disable-line no-unused-vars
|
|
605
|
-
execution = execution || {};
|
|
606
|
-
|
|
607
|
-
// A top-level corrective_insight and a distilled_payload are not mutually
|
|
608
|
-
// exclusive, so do not early-return on the insight alone -- merge both.
|
|
609
|
-
const hostPayload = _hostDistilledPayload(execution);
|
|
610
|
-
if (!execution.corrective_insight && !hostPayload) {
|
|
611
|
-
return execution; // nothing from the host -> mechanical fallback
|
|
612
|
-
}
|
|
613
|
-
const merged = Object.assign({}, execution);
|
|
614
|
-
if (hostPayload) {
|
|
615
|
-
if (!merged.corrective_insight && hostPayload.corrective_insight) {
|
|
616
|
-
merged.corrective_insight = hostPayload.corrective_insight;
|
|
617
|
-
}
|
|
618
|
-
if (hostPayload.strategy) merged.distilled_strategy = hostPayload.strategy;
|
|
619
|
-
if (hostPayload.preconditions) merged.distilled_preconditions = hostPayload.preconditions;
|
|
620
|
-
}
|
|
621
|
-
return merged;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// ---------------------------------------------------------------------------
|
|
625
|
-
// Forgery guard: a Capsule with status=success but no execution evidence is
|
|
626
|
-
// rejected outright. This is the single most important defence against agents
|
|
627
|
-
// "hallucinating" a successful run just to bulk up the community registry.
|
|
628
|
-
// ---------------------------------------------------------------------------
|
|
629
|
-
function detectForgery(execution) {
|
|
630
|
-
const trace = Array.isArray(execution && execution.trace) ? execution.trace : [];
|
|
631
|
-
const blast = execution && execution.blast_radius ? execution.blast_radius : null;
|
|
632
|
-
const files = blast ? Number(blast.files || 0) : 0;
|
|
633
|
-
const lines = blast ? Number(blast.lines || 0) : 0;
|
|
634
|
-
const status = execution && execution.status ? String(execution.status) : 'failed';
|
|
635
|
-
if (status !== 'success') return null;
|
|
636
|
-
if (trace.length === 0) return 'empty_execution_trace';
|
|
637
|
-
if (files === 0 && lines === 0) return 'zero_blast_radius_with_success';
|
|
638
|
-
const anyExitRecorded = trace.some((t) => Number.isInteger(t && t.exit));
|
|
639
|
-
if (!anyExitRecorded) return 'no_exit_code_in_trace';
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// ---------------------------------------------------------------------------
|
|
644
|
-
// Assemble a Capsule from a gene reference + real execution evidence.
|
|
645
|
-
// Cross-references Gene.validation -> execution.trace. If any validation
|
|
646
|
-
// command is missing from the trace, we refuse to emit the Capsule and
|
|
647
|
-
// return a diagnostic instead.
|
|
648
|
-
// ---------------------------------------------------------------------------
|
|
649
|
-
function assembleCapsule(gene, execution, opts) {
|
|
650
|
-
const trace = Array.isArray(execution && execution.trace) ? execution.trace : [];
|
|
651
|
-
const geneValidations = Array.isArray(gene.validation) ? gene.validation : [];
|
|
652
|
-
const traceCmds = new Set(trace.map((t) => normalizeCmd(t && t.cmd)));
|
|
653
|
-
const missing = [];
|
|
654
|
-
geneValidations.forEach((v) => { if (!traceCmds.has(normalizeCmd(v))) missing.push(v); });
|
|
655
|
-
if (missing.length > 0) {
|
|
656
|
-
return { ok: false, reason: 'validation_coverage_missing', missing: missing };
|
|
657
|
-
}
|
|
658
|
-
for (const v of geneValidations) {
|
|
659
|
-
const t = trace.find((tt) => normalizeCmd(tt && tt.cmd) === normalizeCmd(v));
|
|
660
|
-
if (t && !Number.isInteger(t.exit)) {
|
|
661
|
-
return { ok: false, reason: 'validation_missing_exit_code', cmd: v };
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const scoreRaw = execution && execution.score != null ? Number(execution.score) : null;
|
|
666
|
-
const status = execution && execution.status ? String(execution.status) : 'failed';
|
|
667
|
-
let score;
|
|
668
|
-
if (Number.isFinite(scoreRaw)) {
|
|
669
|
-
score = Math.max(0, Math.min(1, scoreRaw));
|
|
670
|
-
} else {
|
|
671
|
-
score = status === 'success' ? 0.8 : 0.2;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const blast = execution && execution.blast_radius ? execution.blast_radius : { files: 0, lines: 0 };
|
|
675
|
-
const env = (envFingerprint && typeof envFingerprint.captureEnvFingerprint === 'function')
|
|
676
|
-
? envFingerprint.captureEnvFingerprint()
|
|
677
|
-
: ((execution && execution.env_fingerprint) || null);
|
|
678
|
-
|
|
679
|
-
// gene.id may have been rewritten by validateSynthesizedGene (e.g. to
|
|
680
|
-
// DISTILLED_ID_PREFIX); extract whatever suffix is there instead of
|
|
681
|
-
// assuming our original SKILL2GEP_ID_PREFIX is still present.
|
|
682
|
-
const geneIdSuffix = String(gene.id).replace(/^gene_[a-z0-9]+_/, '').replace(/^gene_/, '');
|
|
683
|
-
const idKey = shortHash(gene.id + '|' + (execution && execution.started_at || new Date().toISOString()));
|
|
684
|
-
const capsule = {
|
|
685
|
-
type: 'Capsule',
|
|
686
|
-
id: CAPSULE_ID_PREFIX + slugify(geneIdSuffix) + '_' + idKey,
|
|
687
|
-
gene: gene.id,
|
|
688
|
-
trigger: Array.isArray(execution && execution.trigger) ? execution.trigger : (gene.signals_match || []).slice(0, 6),
|
|
689
|
-
summary: (execution && execution.summary) || ('Applied ' + gene.id + ' on scenario ' + (opts && opts.scenario || 'local skill invocation')),
|
|
690
|
-
confidence: Math.max(0, Math.min(1, score)),
|
|
691
|
-
blast_radius: { files: Number(blast.files || 0), lines: Number(blast.lines || 0) },
|
|
692
|
-
outcome: { status: status, score: score },
|
|
693
|
-
success_reason: status === 'success' ? ((execution && execution.success_reason) || 'Skill workflow completed and all declared validations passed.') : null,
|
|
694
|
-
env_fingerprint: env || { os: process.platform, node: process.version },
|
|
695
|
-
source_type: 'skill2gep_hook',
|
|
696
|
-
strategy: Array.isArray(gene.strategy) ? gene.strategy.slice() : [],
|
|
697
|
-
content: (execution && execution.content_summary) || buildContentSummary(trace, blast),
|
|
698
|
-
execution_trace: trace.map((t, i) => ({
|
|
699
|
-
step: Number.isInteger(t && t.step) ? t.step : i + 1,
|
|
700
|
-
cmd: String(t && t.cmd || ''),
|
|
701
|
-
exit: Number.isInteger(t && t.exit) ? t.exit : null,
|
|
702
|
-
stdout_tail: t && t.stdout_tail ? String(t.stdout_tail).slice(0, 300) : '',
|
|
703
|
-
})),
|
|
704
|
-
schema_version: '1.6.0',
|
|
705
|
-
};
|
|
706
|
-
return { ok: true, capsule: capsule };
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function buildContentSummary(trace, blast) {
|
|
710
|
-
const okCount = trace.filter((t) => Number(t && t.exit) === 0).length;
|
|
711
|
-
const files = blast ? Number(blast.files || 0) : 0;
|
|
712
|
-
const lines = blast ? Number(blast.lines || 0) : 0;
|
|
713
|
-
return 'Ran ' + trace.length + ' validation command(s), ' + okCount + ' passed. Blast radius: ' + files + ' files, ' + lines + ' lines.';
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// ---------------------------------------------------------------------------
|
|
717
|
-
// Main entrypoint: runOnSkillInvocation(opts)
|
|
718
|
-
//
|
|
719
|
-
// opts = {
|
|
720
|
-
// skillPath: absolute path to SKILL.md or skill directory (required)
|
|
721
|
-
// skillName: optional, auto-derived from frontmatter otherwise
|
|
722
|
-
// platform: 'cursor' | 'claude-code' | 'codex' | generic (optional)
|
|
723
|
-
// execution: {
|
|
724
|
-
// status: 'success' | 'failed' (REQUIRED for Capsule emission)
|
|
725
|
-
// score: 0..1
|
|
726
|
-
// started_at: ISO8601 string
|
|
727
|
-
// trace: [ { step, cmd, exit, stdout_tail }, ... ]
|
|
728
|
-
// blast_radius: { files, lines }
|
|
729
|
-
// trigger: [ signals actually fired ]
|
|
730
|
-
// signals: [ signals actually detected ]
|
|
731
|
-
// summary: optional one-line result
|
|
732
|
-
// success_reason, env_fingerprint, content_summary -- all optional
|
|
733
|
-
// },
|
|
734
|
-
// publish: boolean (default true, from SKILL2GEP_AUTO_PUBLISH)
|
|
735
|
-
// }
|
|
736
|
-
//
|
|
737
|
-
// Returns {
|
|
738
|
-
// ok: boolean,
|
|
739
|
-
// gene, capsule,
|
|
740
|
-
// capsule_diagnostic, // null, or reason why we refused to emit a Capsule
|
|
741
|
-
// persist_errors, // list of local storage errors (upsert, write state)
|
|
742
|
-
// publish_requested, // true if auto-publish was attempted
|
|
743
|
-
// publish_promise, // Promise<publish result> if publish was fired
|
|
744
|
-
// rationale, // one-line explanation citing the paper
|
|
745
|
-
// reason, errors // set when ok=false
|
|
746
|
-
// }
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
function runOnSkillInvocation(opts) {
|
|
749
|
-
opts = opts || {};
|
|
750
|
-
const skillPath = opts.skillPath;
|
|
751
|
-
if (!skillPath || !fs.existsSync(skillPath)) {
|
|
752
|
-
return { ok: false, reason: 'skill_path_missing', skillPath: skillPath };
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
let skillMdPath = skillPath;
|
|
756
|
-
try {
|
|
757
|
-
const stat = fs.statSync(skillPath);
|
|
758
|
-
if (stat.isDirectory()) skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
759
|
-
} catch (_) { return { ok: false, reason: 'skill_path_unreadable' }; }
|
|
760
|
-
if (!fs.existsSync(skillMdPath)) return { ok: false, reason: 'skill_md_missing', tried: skillMdPath };
|
|
761
|
-
|
|
762
|
-
let skillMd;
|
|
763
|
-
try { skillMd = fs.readFileSync(skillMdPath, 'utf8'); }
|
|
764
|
-
catch (err) { return { ok: false, reason: 'skill_md_read_failed', error: err && err.message ? err.message : String(err) }; }
|
|
765
|
-
const skillHash = shortHash(skillMd);
|
|
766
|
-
|
|
767
|
-
// Idempotency: if we've already distilled this exact skill content + the
|
|
768
|
-
// same execution fingerprint, skip to avoid duplicate community uploads.
|
|
769
|
-
// Include the evolved-trajectory + host-distillation fields in the
|
|
770
|
-
// idempotency key: they change the synthesized Gene, so a later, richer host
|
|
771
|
-
// distillation of the same trace must NOT be short-circuited as
|
|
772
|
-
// already_distilled with a stale Gene.
|
|
773
|
-
const ex0 = opts.execution || {};
|
|
774
|
-
const execHash = shortHash(JSON.stringify({
|
|
775
|
-
trace: ex0.trace || [],
|
|
776
|
-
br: ex0.blast_radius || null,
|
|
777
|
-
status: ex0.status || null,
|
|
778
|
-
mutation_log: ex0.mutation_log || null,
|
|
779
|
-
rollouts: ex0.rollouts || null,
|
|
780
|
-
corrective_insight: ex0.corrective_insight || null,
|
|
781
|
-
distilled_payload: ex0.distilled_payload || null,
|
|
782
|
-
}));
|
|
783
|
-
const state = readState();
|
|
784
|
-
const seenKey = skillHash + ':' + execHash;
|
|
785
|
-
if (state.seen && state.seen[seenKey]) {
|
|
786
|
-
return { ok: false, reason: 'already_distilled', gene: state.seen[seenKey].gene, capsule: state.seen[seenKey].capsule };
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const parsed = parseSkillMd(skillMd);
|
|
790
|
-
|
|
791
|
-
// Consume any distillation the host agent (the LLM) supplied inline on the
|
|
792
|
-
// execution record. This never calls out -- it just promotes a host-provided
|
|
793
|
-
// corrective_insight / distilled_payload onto the execution before synthesis.
|
|
794
|
-
let execution = opts.execution || {};
|
|
795
|
-
try {
|
|
796
|
-
const enriched = distillWithLLM(parsed, execution, { skillMd: skillMd });
|
|
797
|
-
if (enriched) execution = enriched;
|
|
798
|
-
} catch (_) { /* non-fatal: keep the original execution record */ }
|
|
799
|
-
|
|
800
|
-
const geneResult = synthesizeGene(parsed, execution, {
|
|
801
|
-
skillName: opts.skillName || parsed.name,
|
|
802
|
-
platform: opts.platform || null,
|
|
803
|
-
skillHash: skillHash,
|
|
804
|
-
skillMd: skillMd,
|
|
805
|
-
strict: Boolean(opts.strict),
|
|
806
|
-
});
|
|
807
|
-
if (!geneResult.valid) {
|
|
808
|
-
appendJsonl(logPath(), {
|
|
809
|
-
timestamp: new Date().toISOString(), status: 'gene_validation_failed',
|
|
810
|
-
skill: opts.skillName || parsed.name, errors: geneResult.errors,
|
|
811
|
-
});
|
|
812
|
-
return { ok: false, reason: 'gene_validation_failed', errors: geneResult.errors };
|
|
813
|
-
}
|
|
814
|
-
const gene = geneResult.gene;
|
|
815
|
-
|
|
816
|
-
// Quality gate (operationalizes the TaskGenome Bench finding that
|
|
817
|
-
// reference-distilled Genes can be WORSE than Skills). A low-quality
|
|
818
|
-
// distilled/manual Gene is downgraded to Gene-only and flagged; strict mode
|
|
819
|
-
// refuses it. Evolved Genes are never gated. A malformed env value parses to
|
|
820
|
-
// NaN -> treat as "gate disabled" (0) rather than passing everything.
|
|
821
|
-
const minQualityRaw = Number(process.env.SKILL2GEP_MIN_QUALITY);
|
|
822
|
-
const minQuality = Number.isFinite(minQualityRaw) ? minQualityRaw : 0;
|
|
823
|
-
let qualityGate = null;
|
|
824
|
-
if (geneResult.source !== 'evolved' && geneResult.quality_score < minQuality) {
|
|
825
|
-
qualityGate = {
|
|
826
|
-
reason: 'low_quality_distilled_gene',
|
|
827
|
-
source: geneResult.source,
|
|
828
|
-
quality_score: geneResult.quality_score,
|
|
829
|
-
note: 'reference-distilled/manual Gene below SKILL2GEP_MIN_QUALITY; '
|
|
830
|
-
+ 'TaskGenome Bench shows such Genes may underperform the source Skill.',
|
|
831
|
-
};
|
|
832
|
-
if (opts.strict) {
|
|
833
|
-
appendJsonl(logPath(), {
|
|
834
|
-
timestamp: new Date().toISOString(), status: 'quality_gate_rejected',
|
|
835
|
-
skill: opts.skillName || parsed.name, gate: qualityGate,
|
|
836
|
-
});
|
|
837
|
-
return { ok: false, reason: 'quality_gate_rejected', gate: qualityGate };
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
let capsule = null;
|
|
842
|
-
let capsuleDiag = null;
|
|
843
|
-
// A quality-gated Gene is published as Gene-only: do not mint a Capsule that
|
|
844
|
-
// would advertise it as a verified success.
|
|
845
|
-
if (execution && execution.status && !qualityGate) {
|
|
846
|
-
const forgery = detectForgery(execution);
|
|
847
|
-
if (forgery) {
|
|
848
|
-
capsuleDiag = { reason: 'capsule_rejected_forgery', detail: forgery };
|
|
849
|
-
} else {
|
|
850
|
-
const capRes = assembleCapsule(gene, execution, { scenario: opts.scenario || parsed.name });
|
|
851
|
-
if (capRes.ok) capsule = capRes.capsule; else capsuleDiag = capRes;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const persistErrors = [];
|
|
856
|
-
try { assetStore.upsertGene(gene); }
|
|
857
|
-
catch (err) { persistErrors.push({ step: 'upsertGene', error: err && err.message ? err.message : String(err) }); }
|
|
858
|
-
if (capsule) {
|
|
859
|
-
try { assetStore.appendCapsule(capsule); }
|
|
860
|
-
catch (err) { persistErrors.push({ step: 'appendCapsule', error: err && err.message ? err.message : String(err) }); }
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
state.seen = state.seen || {};
|
|
864
|
-
state.seen[seenKey] = {
|
|
865
|
-
at: new Date().toISOString(),
|
|
866
|
-
gene: gene.id,
|
|
867
|
-
capsule: capsule ? capsule.id : null,
|
|
868
|
-
};
|
|
869
|
-
try { writeState(state); } catch (err) { persistErrors.push({ step: 'writeState', error: err && err.message ? err.message : String(err) }); }
|
|
870
|
-
|
|
871
|
-
const shouldPublish = (opts.publish !== false)
|
|
872
|
-
&& String(process.env.SKILL2GEP_AUTO_PUBLISH || 'true').toLowerCase() !== 'false';
|
|
873
|
-
|
|
874
|
-
// Kick off publish in background. We never block the hook on the Hub -- if
|
|
875
|
-
// the network is slow, the hook still exits in bounded time and we log the
|
|
876
|
-
// publish promise's outcome asynchronously.
|
|
877
|
-
let publishPromise = null;
|
|
878
|
-
if (shouldPublish) {
|
|
879
|
-
publishPromise = publishAssets(gene, capsule).then((result) => {
|
|
880
|
-
appendJsonl(logPath(), {
|
|
881
|
-
timestamp: new Date().toISOString(),
|
|
882
|
-
status: 'publish_result',
|
|
883
|
-
skill: opts.skillName || parsed.name,
|
|
884
|
-
gene_id: gene.id,
|
|
885
|
-
capsule_id: capsule ? capsule.id : null,
|
|
886
|
-
publish: result,
|
|
887
|
-
});
|
|
888
|
-
return result;
|
|
889
|
-
}).catch((err) => {
|
|
890
|
-
const fail = { ok: false, error: err && err.message ? err.message : String(err) };
|
|
891
|
-
appendJsonl(logPath(), {
|
|
892
|
-
timestamp: new Date().toISOString(),
|
|
893
|
-
status: 'publish_error',
|
|
894
|
-
skill: opts.skillName || parsed.name,
|
|
895
|
-
gene_id: gene.id,
|
|
896
|
-
capsule_id: capsule ? capsule.id : null,
|
|
897
|
-
publish: fail,
|
|
898
|
-
});
|
|
899
|
-
return fail;
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
appendJsonl(logPath(), {
|
|
904
|
-
timestamp: new Date().toISOString(),
|
|
905
|
-
status: 'distilled',
|
|
906
|
-
skill: opts.skillName || parsed.name,
|
|
907
|
-
gene_id: gene.id,
|
|
908
|
-
generation_source: geneResult.source,
|
|
909
|
-
quality_score: geneResult.quality_score,
|
|
910
|
-
quality_gate: qualityGate,
|
|
911
|
-
leakage_audit: geneResult.audit,
|
|
912
|
-
capsule_id: capsule ? capsule.id : null,
|
|
913
|
-
capsule_diagnostic: capsuleDiag,
|
|
914
|
-
persist_errors: persistErrors,
|
|
915
|
-
published_requested: shouldPublish,
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
return {
|
|
919
|
-
ok: true,
|
|
920
|
-
gene: gene,
|
|
921
|
-
capsule: capsule,
|
|
922
|
-
generation_source: geneResult.source,
|
|
923
|
-
quality_score: geneResult.quality_score,
|
|
924
|
-
quality_gate: qualityGate,
|
|
925
|
-
leakage_audit: geneResult.audit,
|
|
926
|
-
capsule_diagnostic: capsuleDiag,
|
|
927
|
-
persist_errors: persistErrors,
|
|
928
|
-
publish_requested: shouldPublish,
|
|
929
|
-
publish_promise: publishPromise,
|
|
930
|
-
rationale: RATIONALE_TEXT,
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// ---------------------------------------------------------------------------
|
|
935
|
-
// Community upload. Two channels, both best-effort:
|
|
936
|
-
//
|
|
937
|
-
// 1. Skill Store: skillPublisher.publishSkillToHub() converts the Gene into a
|
|
938
|
-
// SKILL.md and POSTs it to /a2a/skill/store/publish. This is the human-
|
|
939
|
-
// facing channel that also serves as a Gene index.
|
|
940
|
-
//
|
|
941
|
-
// 2. GEP publish bundle: a2a.buildPublishBundle({gene, capsule}) signs both
|
|
942
|
-
// assets with the node secret and a2a.httpTransportSend() POSTs them to
|
|
943
|
-
// /a2a/publish (the A2A message_type routing). This is the auditable
|
|
944
|
-
// machine-facing channel used by solidify.js for normal capsule
|
|
945
|
-
// publishing.
|
|
946
|
-
//
|
|
947
|
-
// We always try channel 1 for the Gene; channel 2 only runs if a real Capsule
|
|
948
|
-
// is attached (Gene-only bundles are not supported by the A2A schema). Each
|
|
949
|
-
// channel's failure is isolated so a broken one cannot block the other.
|
|
950
|
-
// ---------------------------------------------------------------------------
|
|
951
|
-
// publishAssets runs two independent publish channels in parallel:
|
|
952
|
-
// skill_store: skillPublisher.publishSkillToHub (human-facing Gene index)
|
|
953
|
-
// gep_bundle: a2a.httpTransportSend (machine-facing auditable channel)
|
|
954
|
-
// ok is true only when at least one channel succeeds with a real (non-dry-run) publish.
|
|
955
|
-
// When HUB_DRY_RUN is active, both channels short-circuit and ok is false;
|
|
956
|
-
// callers should check result.dry_run to distinguish from a real failure.
|
|
957
|
-
function publishAssets(gene, capsule) {
|
|
958
|
-
const skillPromise = publishSkillChannel(gene);
|
|
959
|
-
const bundlePromise = capsule ? publishBundleChannel(gene, capsule) : Promise.resolve({ ok: false, skipped: 'no_capsule' });
|
|
960
|
-
return Promise.all([skillPromise, bundlePromise]).then(([skill, bundle]) => ({
|
|
961
|
-
skill_store: skill,
|
|
962
|
-
gep_bundle: bundle,
|
|
963
|
-
ok: Boolean((skill && skill.ok && !skill.dry_run) || (bundle && bundle.ok && !bundle.dry_run)),
|
|
964
|
-
dry_run: Boolean((skill && skill.dry_run) || (bundle && bundle.dry_run)),
|
|
965
|
-
}));
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function publishSkillChannel(gene) {
|
|
969
|
-
if (a2a._isDryRun()) return Promise.resolve({ ok: true, dry_run: true });
|
|
970
|
-
try {
|
|
971
|
-
const p = skillPublisher.publishSkillToHub(gene);
|
|
972
|
-
return Promise.resolve(p).catch((err) => ({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
973
|
-
} catch (err) {
|
|
974
|
-
return Promise.resolve({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function publishBundleChannel(gene, capsule) {
|
|
979
|
-
const hubUrl = a2a.getHubUrl && a2a.getHubUrl();
|
|
980
|
-
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
|
|
981
|
-
let message;
|
|
982
|
-
let capsuleClone;
|
|
983
|
-
try {
|
|
984
|
-
// buildPublishBundle mutates asset_id on the objects it receives, so
|
|
985
|
-
// clone first to avoid polluting the locally stored gene/capsule.
|
|
986
|
-
// Also sanitize the clone before publishing so the recall verifier
|
|
987
|
-
// can index by the same hash the Hub will store. Without client-side
|
|
988
|
-
// sanitize, Hub's server-side PII redaction silently rewrites the
|
|
989
|
-
// body and recomputes a different asset_id; the verifier then looks
|
|
990
|
-
// up a hash that does not exist on Hub, producing persistent
|
|
991
|
-
// roundtrip_missing. Mirrors solidify.js publish paths.
|
|
992
|
-
// (Bugbot review on PR #53 round 3.)
|
|
993
|
-
const geneClone = JSON.parse(JSON.stringify(gene));
|
|
994
|
-
capsuleClone = JSON.parse(JSON.stringify(capsule));
|
|
995
|
-
try {
|
|
996
|
-
const { sanitizePayload } = require('./sanitize');
|
|
997
|
-
capsuleClone = sanitizePayload(capsuleClone);
|
|
998
|
-
// Note: do NOT compute asset_id here. buildPublishBundle stamps the
|
|
999
|
-
// canonical asset_id (after also potentially adding execution_trace
|
|
1000
|
-
// and model_name). Computing a hash here would be discarded — and if
|
|
1001
|
-
// buildPublishBundle adds execution_trace, the pre-hash would silently
|
|
1002
|
-
// disagree with the post-hash. The verifier-enqueue closure below reads
|
|
1003
|
-
// capsuleClone.asset_id AFTER buildPublishBundle returns.
|
|
1004
|
-
// (Bugbot review on PR #53 round 4.)
|
|
1005
|
-
} catch (sanitizeErr) {
|
|
1006
|
-
// sanitize is best-effort here; if it fails the unsanitized clone
|
|
1007
|
-
// still publishes — Hub's own PII redaction will still kick in.
|
|
1008
|
-
// Log so the operator can investigate but do not abort the publish.
|
|
1009
|
-
console.log('[skill2gep] sanitize failed (non-fatal): ' + (sanitizeErr && sanitizeErr.message || sanitizeErr));
|
|
1010
|
-
}
|
|
1011
|
-
message = a2a.buildPublishBundle({ gene: geneClone, capsule: capsuleClone });
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
return Promise.resolve({ ok: false, error: 'build_publish_bundle_failed: ' + (err && err.message ? err.message : String(err)) });
|
|
1014
|
-
}
|
|
1015
|
-
try {
|
|
1016
|
-
const send = a2a.httpTransportSend(message, { hubUrl: hubUrl, timeoutMs: 15000 });
|
|
1017
|
-
return Promise.resolve(send)
|
|
1018
|
-
.then(function (res) {
|
|
1019
|
-
if (res && res.ok && !res.dry_run) {
|
|
1020
|
-
try {
|
|
1021
|
-
require('./recallVerifier').enqueuePublishedAsset({
|
|
1022
|
-
asset_id: (capsuleClone && capsuleClone.asset_id) || capsule.asset_id,
|
|
1023
|
-
type: 'SkillBundle',
|
|
1024
|
-
signals: Array.isArray(capsule.trigger) ? capsule.trigger : [],
|
|
1025
|
-
publishedAt: Date.now(),
|
|
1026
|
-
});
|
|
1027
|
-
} catch (rvErr) { /* non-fatal */ }
|
|
1028
|
-
}
|
|
1029
|
-
return res;
|
|
1030
|
-
})
|
|
1031
|
-
.catch((err) => ({ ok: false, error: err && err.message ? err.message : String(err) }));
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
return Promise.resolve({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
module.exports = {
|
|
1038
|
-
SKILL2GEP_ID_PREFIX,
|
|
1039
|
-
CAPSULE_ID_PREFIX,
|
|
1040
|
-
RATIONALE_LINKS,
|
|
1041
|
-
RATIONALE_TEXT,
|
|
1042
|
-
parseSkillMd,
|
|
1043
|
-
classifyProvenance,
|
|
1044
|
-
synthesizeGene,
|
|
1045
|
-
distillWithLLM,
|
|
1046
|
-
inferCategory,
|
|
1047
|
-
detectForgery,
|
|
1048
|
-
assembleCapsule,
|
|
1049
|
-
runOnSkillInvocation,
|
|
1050
|
-
publishAssets,
|
|
1051
|
-
publishSkillChannel,
|
|
1052
|
-
publishBundleChannel,
|
|
1053
|
-
logPath,
|
|
1054
|
-
statePath,
|
|
1055
|
-
DEFAULT_HOOK_TIMEOUT_MS,
|
|
1056
|
-
};
|