@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.
Files changed (231) hide show
  1. package/README.md +27 -564
  2. package/bin/evolver-llm-proxy.js +3 -0
  3. package/bin/evolver-mcp.js +2 -0
  4. package/bin/evolver-proxy.js +3 -0
  5. package/bin/evolver.js +4 -0
  6. package/index.js +1 -3586
  7. package/package.json +32 -62
  8. package/CONTRIBUTING.md +0 -19
  9. package/LICENSE +0 -641
  10. package/README.ja-JP.md +0 -521
  11. package/README.ko-KR.md +0 -520
  12. package/README.zh-CN.md +0 -531
  13. package/SKILL.md +0 -365
  14. package/assets/cover.png +0 -0
  15. package/assets/gep/genes.seed.json +0 -496
  16. package/conformance/savings-core/constants.json +0 -30
  17. package/conformance/savings-core/golden-vectors.json +0 -333
  18. package/scripts/a2a_export.js +0 -63
  19. package/scripts/a2a_ingest.js +0 -79
  20. package/scripts/a2a_promote.js +0 -118
  21. package/scripts/analyze_by_skill.js +0 -121
  22. package/scripts/build_binaries.js +0 -479
  23. package/scripts/check-changelog.js +0 -166
  24. package/scripts/extract_log.js +0 -85
  25. package/scripts/generate_history.js +0 -75
  26. package/scripts/gep_append_event.js +0 -96
  27. package/scripts/gep_personality_report.js +0 -234
  28. package/scripts/human_report.js +0 -147
  29. package/scripts/recall-verify-report.js +0 -234
  30. package/scripts/recover_loop.js +0 -61
  31. package/scripts/refresh_stars_badge.js +0 -168
  32. package/scripts/seed-merchants.js +0 -91
  33. package/scripts/skill2recipes.js +0 -118
  34. package/scripts/suggest_version.js +0 -89
  35. package/scripts/validate-modules.js +0 -38
  36. package/scripts/validate-suite.js +0 -78
  37. package/skills/_meta/SKILL.md +0 -41
  38. package/skills/index.json +0 -14
  39. package/src/adapters/claudeCode.js +0 -194
  40. package/src/adapters/codex.js +0 -216
  41. package/src/adapters/cursor.js +0 -91
  42. package/src/adapters/hookAdapter.js +0 -469
  43. package/src/adapters/kiro.js +0 -195
  44. package/src/adapters/opencode.js +0 -326
  45. package/src/adapters/scripts/_lockPaths.js +0 -74
  46. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  47. package/src/adapters/scripts/_runtimePaths.js +0 -440
  48. package/src/adapters/scripts/evolver-session-end.js +0 -321
  49. package/src/adapters/scripts/evolver-session-start.js +0 -587
  50. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  51. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  52. package/src/atp/atpExecute.js +0 -283
  53. package/src/atp/atpTaskPickup.js +0 -233
  54. package/src/atp/autoBuyer.js +0 -382
  55. package/src/atp/autoDeliver.js +0 -215
  56. package/src/atp/cli.js +0 -354
  57. package/src/atp/cliAutobuyPrompt.js +0 -154
  58. package/src/atp/consumerAgent.js +0 -157
  59. package/src/atp/defaultHandler.js +0 -69
  60. package/src/atp/heartbeatSignalsHandler.js +0 -254
  61. package/src/atp/hubClient.js +0 -317
  62. package/src/atp/index.js +0 -38
  63. package/src/atp/merchantAgent.js +0 -118
  64. package/src/atp/protocol.js +0 -41
  65. package/src/atp/questionComposer.js +0 -133
  66. package/src/atp/serviceHelper.js +0 -92
  67. package/src/canary.js +0 -13
  68. package/src/config.js +0 -351
  69. package/src/evolve/guards.js +0 -1
  70. package/src/evolve/pipeline/collect.js +0 -1
  71. package/src/evolve/pipeline/dispatch.js +0 -1
  72. package/src/evolve/pipeline/enrich.js +0 -1
  73. package/src/evolve/pipeline/hub.js +0 -1
  74. package/src/evolve/pipeline/select.js +0 -1
  75. package/src/evolve/pipeline/signals.js +0 -1
  76. package/src/evolve/utils.js +0 -1
  77. package/src/evolve.js +0 -1
  78. package/src/experiment/agentRunner.js +0 -229
  79. package/src/experiment/cli.js +0 -159
  80. package/src/experiment/comparison.js +0 -233
  81. package/src/experiment/metrics.js +0 -75
  82. package/src/forceUpdate.js +0 -991
  83. package/src/gep/a2a.js +0 -173
  84. package/src/gep/a2aProtocol.js +0 -1
  85. package/src/gep/analyzer.js +0 -35
  86. package/src/gep/antiAbuseTelemetry.js +0 -1
  87. package/src/gep/assetCallLog.js +0 -197
  88. package/src/gep/assetStore.js +0 -723
  89. package/src/gep/assets.js +0 -36
  90. package/src/gep/autoDistillConv.js +0 -1
  91. package/src/gep/autoDistillLlm.js +0 -1
  92. package/src/gep/bridge.js +0 -138
  93. package/src/gep/candidateEval.js +0 -1
  94. package/src/gep/candidates.js +0 -1
  95. package/src/gep/claimNudge.js +0 -121
  96. package/src/gep/cliContracts.js +0 -1190
  97. package/src/gep/contentHash.js +0 -1
  98. package/src/gep/conversationDistiller.js +0 -1
  99. package/src/gep/conversationSniffer.js +0 -1
  100. package/src/gep/crypto.js +0 -1
  101. package/src/gep/curriculum.js +0 -1
  102. package/src/gep/deviceId.js +0 -1
  103. package/src/gep/directoryClient.js +0 -115
  104. package/src/gep/envFingerprint.js +0 -1
  105. package/src/gep/epigenetics.js +0 -1
  106. package/src/gep/execBridge.js +0 -1
  107. package/src/gep/executionTrace.js +0 -291
  108. package/src/gep/explore.js +0 -1
  109. package/src/gep/featureFlags.js +0 -121
  110. package/src/gep/gitOps.js +0 -265
  111. package/src/gep/hash.js +0 -1
  112. package/src/gep/hostErrorClassifier.js +0 -34
  113. package/src/gep/hubFetch.js +0 -1
  114. package/src/gep/hubReview.js +0 -1
  115. package/src/gep/hubSearch.js +0 -1
  116. package/src/gep/hubVerify.js +0 -1
  117. package/src/gep/idleScheduler.js +0 -400
  118. package/src/gep/issueReporter.js +0 -416
  119. package/src/gep/learningSignals.js +0 -1
  120. package/src/gep/llmReview.js +0 -92
  121. package/src/gep/localStateAwareness.js +0 -243
  122. package/src/gep/mailboxTransport.js +0 -119
  123. package/src/gep/memoryGraph.js +0 -1
  124. package/src/gep/memoryGraphAdapter.js +0 -1
  125. package/src/gep/mutation.js +0 -1
  126. package/src/gep/narrativeMemory.js +0 -1
  127. package/src/gep/oauthLogin.js +0 -181
  128. package/src/gep/openPRRegistry.js +0 -1
  129. package/src/gep/paths.js +0 -522
  130. package/src/gep/personality.js +0 -1
  131. package/src/gep/policyCheck.js +0 -1
  132. package/src/gep/portable.js +0 -103
  133. package/src/gep/privacyClient.js +0 -235
  134. package/src/gep/prompt.js +0 -1
  135. package/src/gep/questionGenerator.js +0 -518
  136. package/src/gep/recallInject.js +0 -1
  137. package/src/gep/recallVerifier.js +0 -1
  138. package/src/gep/reflection.js +0 -1
  139. package/src/gep/sanitize.js +0 -264
  140. package/src/gep/savingsCore.js +0 -1
  141. package/src/gep/schemas/capsule.js +0 -170
  142. package/src/gep/schemas/gene.js +0 -154
  143. package/src/gep/schemas/index.js +0 -8
  144. package/src/gep/schemas/protocol.js +0 -51
  145. package/src/gep/schemas/task.js +0 -74
  146. package/src/gep/selector.js +0 -1
  147. package/src/gep/selfPR.js +0 -469
  148. package/src/gep/signals.js +0 -776
  149. package/src/gep/skill2gep.js +0 -1056
  150. package/src/gep/skill2gepAudit.js +0 -303
  151. package/src/gep/skill2recipes.js +0 -511
  152. package/src/gep/skillDistiller.js +0 -1
  153. package/src/gep/skillPublisher.js +0 -358
  154. package/src/gep/solidify.js +0 -1
  155. package/src/gep/strategy.js +0 -1
  156. package/src/gep/taskReceiver.js +0 -575
  157. package/src/gep/tokenSavings.js +0 -1
  158. package/src/gep/trajectoryExport.js +0 -1
  159. package/src/gep/validationReport.js +0 -55
  160. package/src/gep/validator/index.js +0 -411
  161. package/src/gep/validator/reporter.js +0 -210
  162. package/src/gep/validator/sandboxExecutor.js +0 -480
  163. package/src/gep/validator/stakeBootstrap.js +0 -357
  164. package/src/gep/workspaceKeychain.js +0 -1
  165. package/src/ops/cleanup.js +0 -80
  166. package/src/ops/commentary.js +0 -60
  167. package/src/ops/health_check.js +0 -104
  168. package/src/ops/index.js +0 -11
  169. package/src/ops/innovation.js +0 -67
  170. package/src/ops/lifecycle.js +0 -798
  171. package/src/ops/self_repair.js +0 -76
  172. package/src/ops/skills_monitor.js +0 -147
  173. package/src/ops/trigger.js +0 -33
  174. package/src/proxy/clientSettings.js +0 -405
  175. package/src/proxy/envelope.js +0 -59
  176. package/src/proxy/extensions/dmHandler.js +0 -45
  177. package/src/proxy/extensions/sessionHandler.js +0 -141
  178. package/src/proxy/extensions/skillUpdater.js +0 -64
  179. package/src/proxy/extensions/traceControl.js +0 -1
  180. package/src/proxy/index.js +0 -1395
  181. package/src/proxy/inject.js +0 -1
  182. package/src/proxy/lifecycle/manager.js +0 -1568
  183. package/src/proxy/mailbox/state.js +0 -207
  184. package/src/proxy/mailbox/store.js +0 -590
  185. package/src/proxy/router/cache_passthrough.js +0 -26
  186. package/src/proxy/router/features.js +0 -84
  187. package/src/proxy/router/gemini_route.js +0 -154
  188. package/src/proxy/router/messages_route.js +0 -535
  189. package/src/proxy/router/model_router.js +0 -113
  190. package/src/proxy/router/models_route.js +0 -52
  191. package/src/proxy/router/ollama_route.js +0 -103
  192. package/src/proxy/router/responses_route.js +0 -170
  193. package/src/proxy/router/vertex_route.js +0 -110
  194. package/src/proxy/server/http.js +0 -363
  195. package/src/proxy/server/routes.js +0 -558
  196. package/src/proxy/server/settings.js +0 -115
  197. package/src/proxy/sync/engine.js +0 -179
  198. package/src/proxy/sync/inbound.js +0 -211
  199. package/src/proxy/sync/outbound.js +0 -320
  200. package/src/proxy/task/monitor.js +0 -131
  201. package/src/proxy/trace/extractor.js +0 -1
  202. package/src/proxy/trace/usage.js +0 -1
  203. package/src/webui/client/clientJs/assets.js +0 -111
  204. package/src/webui/client/clientJs/bootstrap.js +0 -92
  205. package/src/webui/client/clientJs/common.js +0 -77
  206. package/src/webui/client/clientJs/i18n.js +0 -366
  207. package/src/webui/client/clientJs/index.js +0 -35
  208. package/src/webui/client/clientJs/interactions.js +0 -351
  209. package/src/webui/client/clientJs/overview.js +0 -152
  210. package/src/webui/client/clientJs/personality.js +0 -285
  211. package/src/webui/client/clientJs/pipelines.js +0 -330
  212. package/src/webui/client/indexHtml.js +0 -221
  213. package/src/webui/client/static.js +0 -23
  214. package/src/webui/client/stylesCss.js +0 -639
  215. package/src/webui/client/vendor/README.md +0 -15
  216. package/src/webui/client/vendor/echarts.min.js +0 -45
  217. package/src/webui/index.js +0 -14
  218. package/src/webui/observer/assets.js +0 -146
  219. package/src/webui/observer/index.js +0 -37
  220. package/src/webui/observer/interactions.js +0 -127
  221. package/src/webui/observer/jsonl.js +0 -75
  222. package/src/webui/observer/paths.js +0 -46
  223. package/src/webui/observer/personality.js +0 -43
  224. package/src/webui/observer/pipelineEvents.js +0 -58
  225. package/src/webui/observer/redact.js +0 -63
  226. package/src/webui/observer/runs.js +0 -356
  227. package/src/webui/observer/safety.js +0 -57
  228. package/src/webui/observer/skills.js +0 -70
  229. package/src/webui/observer/status.js +0 -71
  230. package/src/webui/server/http.js +0 -138
  231. package/src/webui/server/routes.js +0 -41
@@ -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
- };