@evomap/evolver 1.89.18 → 1.89.19

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 (64) hide show
  1. package/index.js +183 -1
  2. package/package.json +7 -2
  3. package/src/adapters/claudeCode.js +8 -39
  4. package/src/adapters/hookAdapter.js +68 -5
  5. package/src/adapters/scripts/evolver-session-start.js +98 -0
  6. package/src/evolve/guards.js +1 -1
  7. package/src/evolve/pipeline/collect.js +1 -1
  8. package/src/evolve/pipeline/dispatch.js +1 -1
  9. package/src/evolve/pipeline/enrich.js +1 -1
  10. package/src/evolve/pipeline/hub.js +1 -1
  11. package/src/evolve/pipeline/select.js +1 -1
  12. package/src/evolve/pipeline/signals.js +1 -1
  13. package/src/evolve/utils.js +1 -1
  14. package/src/evolve.js +1 -1
  15. package/src/gep/a2aProtocol.js +1 -1
  16. package/src/gep/antiAbuseTelemetry.js +1 -1
  17. package/src/gep/autoDistillConv.js +1 -1
  18. package/src/gep/autoDistillLlm.js +1 -1
  19. package/src/gep/candidateEval.js +1 -1
  20. package/src/gep/candidates.js +1 -1
  21. package/src/gep/contentHash.js +1 -1
  22. package/src/gep/conversationDistiller.js +1 -1
  23. package/src/gep/conversationSniffer.js +1 -1
  24. package/src/gep/crypto.js +1 -1
  25. package/src/gep/curriculum.js +1 -1
  26. package/src/gep/deviceId.js +1 -1
  27. package/src/gep/envFingerprint.js +1 -1
  28. package/src/gep/epigenetics.js +1 -1
  29. package/src/gep/execBridge.js +1 -1
  30. package/src/gep/explore.js +1 -1
  31. package/src/gep/hash.js +1 -1
  32. package/src/gep/hubFetch.js +1 -1
  33. package/src/gep/hubReview.js +1 -1
  34. package/src/gep/hubSearch.js +1 -1
  35. package/src/gep/hubVerify.js +1 -1
  36. package/src/gep/learningSignals.js +1 -1
  37. package/src/gep/memoryGraph.js +1 -1
  38. package/src/gep/memoryGraphAdapter.js +1 -1
  39. package/src/gep/mutation.js +1 -1
  40. package/src/gep/narrativeMemory.js +1 -1
  41. package/src/gep/openPRRegistry.js +1 -1
  42. package/src/gep/personality.js +1 -1
  43. package/src/gep/policyCheck.js +1 -1
  44. package/src/gep/prompt.js +1 -1
  45. package/src/gep/recallInject.js +1 -1
  46. package/src/gep/recallVerifier.js +1 -1
  47. package/src/gep/reflection.js +1 -1
  48. package/src/gep/savingsCore.js +1 -1
  49. package/src/gep/selector.js +1 -1
  50. package/src/gep/skill2gep.js +329 -43
  51. package/src/gep/skill2gepAudit.js +303 -0
  52. package/src/gep/skillDistiller.js +1 -1
  53. package/src/gep/solidify.js +1 -1
  54. package/src/gep/strategy.js +1 -1
  55. package/src/gep/tokenSavings.js +1 -1
  56. package/src/gep/trajectoryExport.js +1 -0
  57. package/src/gep/workspaceKeychain.js +1 -1
  58. package/src/proxy/extensions/traceControl.js +1 -1
  59. package/src/proxy/index.js +3 -0
  60. package/src/proxy/inject.js +1 -1
  61. package/src/proxy/router/messages_route.js +52 -1
  62. package/src/proxy/sync/outbound.js +7 -2
  63. package/src/proxy/trace/extractor.js +1 -1
  64. package/src/proxy/trace/usage.js +1 -1
@@ -29,6 +29,7 @@ const skillDistiller = require('./skillDistiller');
29
29
  const skillPublisher = require('./skillPublisher');
30
30
  const envFingerprint = require('./envFingerprint');
31
31
  const a2a = require('./a2aProtocol');
32
+ const audit = require('./skill2gepAudit');
32
33
 
33
34
  const SKILL2GEP_ID_PREFIX = 'gene_s2g_';
34
35
 
@@ -261,22 +262,133 @@ function parseSkillMd(skillMd) {
261
262
  };
262
263
  }
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
+
264
361
  // ---------------------------------------------------------------------------
265
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
+ //
266
369
  // Validation is delegated to skillDistiller.validateSynthesizedGene() so that
267
370
  // we reuse the sanitization, ID-rewrite, forbidden-path, and validation-cmd
268
371
  // policy rules already hardened there.
269
372
  // ---------------------------------------------------------------------------
270
373
  function synthesizeGene(parsed, execution, opts) {
271
- const traceSignals = Array.isArray(execution && execution.signals) ? execution.signals : [];
374
+ execution = execution || {};
375
+ opts = opts || {};
376
+ const traceSignals = Array.isArray(execution.signals) ? execution.signals : [];
272
377
  const mergedSignals = Array.from(new Set([].concat(parsed.signals_match || [], traceSignals)));
273
378
 
274
- // AVOID items live in their own top-level `avoid` field on the Gene, NOT as
275
- // synthetic "AVOID: ..." strategy steps. Skill Store / Hub renderers should
276
- // surface them in a dedicated "## Avoid" section so downstream consumers
277
- // never mistake anti-patterns for positive steps.
278
- const strategy = [];
279
- (parsed.strategy || []).forEach((s) => strategy.push(s));
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
+ }
280
392
  if (strategy.length < 3) {
281
393
  strategy.push('Identify the dominant trigger signals from the Skill description.');
282
394
  strategy.push('Apply the smallest targeted change that satisfies the Skill workflow.');
@@ -284,24 +396,41 @@ function synthesizeGene(parsed, execution, opts) {
284
396
  }
285
397
  const avoid = Array.isArray(parsed.avoid) ? parsed.avoid.slice(0, 5) : [];
286
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
+
287
416
  // Filter validation commands through the same allow-list that
288
- // validateSynthesizedGene will later apply (node/npm/npx only). If the
289
- // skill's original validation lines are all blocked (e.g. pytest, bash)
290
- // we would end up with an empty gene.validation, which would silently
291
- // defeat the Capsule coverage check. In that case, behavior depends on
292
- // strict mode:
293
- // - strict=true -> refuse to synthesize; caller gets an explicit error.
294
- // - strict=false -> fall back to a concrete but near-trivial 'node --version'
295
- // so Gene.validation is never empty. The quality
296
- // heuristics field records that a fallback was used.
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.
297
427
  const policyCheck = require('./policyCheck');
298
428
  const rawValidations = Array.isArray(parsed.validation) ? parsed.validation : [];
299
429
  const allowedValidations = rawValidations
300
430
  .map((v) => String(v || '').trim())
301
431
  .filter((v) => v && policyCheck.isValidationCommandAllowed(v));
302
- const fallbackUsed = allowedValidations.length === 0;
303
- const strict = Boolean(opts && opts.strict);
304
- if (strict && fallbackUsed) {
432
+ const validation = allowedValidations;
433
+ if (Boolean(opts.strict) && validation.length === 0) {
305
434
  return {
306
435
  valid: false,
307
436
  errors: [
@@ -310,59 +439,112 @@ function synthesizeGene(parsed, execution, opts) {
310
439
  + 'Rewrite the Skill\'s validation section with those, or drop --strict.',
311
440
  ],
312
441
  gene: null,
442
+ source: source,
313
443
  };
314
444
  }
315
- const validation = fallbackUsed ? ['node --version'] : allowedValidations;
316
445
 
317
- // Quality heuristics: lightweight signals for downstream reviewers (and the
318
- // paper-assumption disclaimer). These do NOT guarantee Gene quality; they
319
- // only describe how much signal we managed to extract from the source Skill.
320
- const avoidCount = (parsed.avoid || []).length;
321
- const strategySteps = (parsed.strategy || []).length;
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);
322
449
  const qualityHeuristics = {
323
- strategy_steps: strategySteps,
324
- avoid_count: avoidCount,
450
+ strategy_steps: (parsed.strategy || []).length,
451
+ avoid_count: (parsed.avoid || []).length,
325
452
  validation_declared_count: rawValidations.length,
326
453
  validation_runnable_count: allowedValidations.length,
327
- validation_fallback_used: fallbackUsed,
328
454
  signals_extracted: (parsed.signals_match || []).length,
329
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),
330
460
  };
331
461
 
332
- const skillSlug = slugify(parsed.name || (opts && opts.skillName) || 'skill');
333
- const draft = {
462
+ const skillSlug = slugify(parsed.name || opts.skillName || 'skill');
463
+ let draft = {
334
464
  type: 'Gene',
335
465
  id: SKILL2GEP_ID_PREFIX + skillSlug,
336
466
  summary: (parsed.description || strategy[0] || 'Reusable strategy distilled from Skill').slice(0, 200),
337
467
  category: inferCategory(mergedSignals, parsed.description),
338
468
  signals_match: mergedSignals.slice(0, 8),
339
- preconditions: (parsed.preconditions && parsed.preconditions.length > 0)
340
- ? parsed.preconditions
341
- : ['Skill ' + (parsed.name || 'unknown') + ' has just been executed locally'],
469
+ preconditions: preconditions.slice(0, 6),
342
470
  strategy: strategy.slice(0, MAX_STRATEGY_STEPS),
343
471
  avoid: avoid,
344
472
  constraints: {
345
- max_files: (opts && opts.maxFiles) || skillDistiller.DISTILLED_MAX_FILES,
473
+ max_files: opts.maxFiles || skillDistiller.DISTILLED_MAX_FILES,
346
474
  forbidden_paths: ['.git', 'node_modules'],
347
475
  },
348
476
  validation: validation,
349
477
  schema_version: '1.6.0',
350
478
  _source: {
351
479
  kind: 'skill2gep',
480
+ generation_source: source,
352
481
  skill_name: parsed.name || null,
353
- skill_platform: (opts && opts.platform) || null,
354
- skill_hash: opts && opts.skillHash ? opts.skillHash : null,
482
+ skill_platform: opts.platform || null,
483
+ skill_hash: opts.skillHash ? opts.skillHash : null,
355
484
  rationale_paper: RATIONALE_LINKS.paper,
356
485
  paper_scope: 'code-science (arXiv:2604.15097, 45 tasks, Gemini 3.1 Pro/Flash Lite)',
357
486
  claims_outside_scope: 'assumption',
487
+ quality_score: qualityScore,
488
+ overcame_errors: Array.isArray(execution.mutation_log) ? execution.mutation_log.slice(0, 8) : [],
358
489
  quality_heuristics: qualityHeuristics,
359
490
  },
360
491
  };
361
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
+
362
540
  const assetsDir = paths.getGepAssetsDir();
363
541
  const existingGenesJson = readJsonSafe(path.join(assetsDir, 'genes.json'), { genes: [] });
364
542
  const existingGenes = Array.isArray(existingGenesJson.genes) ? existingGenesJson.genes : [];
365
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;
366
548
  return result;
367
549
  }
368
550
 
@@ -392,6 +574,53 @@ function inferCategory(signals, description) {
392
574
  return 'optimize';
393
575
  }
394
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
+
395
624
  // ---------------------------------------------------------------------------
396
625
  // Forgery guard: a Capsule with status=success but no execution evidence is
397
626
  // rejected outright. This is the single most important defence against agents
@@ -537,10 +766,19 @@ function runOnSkillInvocation(opts) {
537
766
 
538
767
  // Idempotency: if we've already distilled this exact skill content + the
539
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 || {};
540
774
  const execHash = shortHash(JSON.stringify({
541
- trace: (opts.execution && opts.execution.trace) || [],
542
- br: opts.execution && opts.execution.blast_radius || null,
543
- status: opts.execution && opts.execution.status || null,
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,
544
782
  }));
545
783
  const state = readState();
546
784
  const seenKey = skillHash + ':' + execHash;
@@ -549,10 +787,21 @@ function runOnSkillInvocation(opts) {
549
787
  }
550
788
 
551
789
  const parsed = parseSkillMd(skillMd);
552
- const geneResult = synthesizeGene(parsed, opts.execution || {}, {
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, {
553
801
  skillName: opts.skillName || parsed.name,
554
802
  platform: opts.platform || null,
555
803
  skillHash: skillHash,
804
+ skillMd: skillMd,
556
805
  strict: Boolean(opts.strict),
557
806
  });
558
807
  if (!geneResult.valid) {
@@ -564,14 +813,41 @@ function runOnSkillInvocation(opts) {
564
813
  }
565
814
  const gene = geneResult.gene;
566
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
+
567
841
  let capsule = null;
568
842
  let capsuleDiag = null;
569
- if (opts.execution && opts.execution.status) {
570
- const forgery = detectForgery(opts.execution);
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);
571
847
  if (forgery) {
572
848
  capsuleDiag = { reason: 'capsule_rejected_forgery', detail: forgery };
573
849
  } else {
574
- const capRes = assembleCapsule(gene, opts.execution, { scenario: opts.scenario || parsed.name });
850
+ const capRes = assembleCapsule(gene, execution, { scenario: opts.scenario || parsed.name });
575
851
  if (capRes.ok) capsule = capRes.capsule; else capsuleDiag = capRes;
576
852
  }
577
853
  }
@@ -629,6 +905,10 @@ function runOnSkillInvocation(opts) {
629
905
  status: 'distilled',
630
906
  skill: opts.skillName || parsed.name,
631
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,
632
912
  capsule_id: capsule ? capsule.id : null,
633
913
  capsule_diagnostic: capsuleDiag,
634
914
  persist_errors: persistErrors,
@@ -639,6 +919,10 @@ function runOnSkillInvocation(opts) {
639
919
  ok: true,
640
920
  gene: gene,
641
921
  capsule: capsule,
922
+ generation_source: geneResult.source,
923
+ quality_score: geneResult.quality_score,
924
+ quality_gate: qualityGate,
925
+ leakage_audit: geneResult.audit,
642
926
  capsule_diagnostic: capsuleDiag,
643
927
  persist_errors: persistErrors,
644
928
  publish_requested: shouldPublish,
@@ -756,7 +1040,9 @@ module.exports = {
756
1040
  RATIONALE_LINKS,
757
1041
  RATIONALE_TEXT,
758
1042
  parseSkillMd,
1043
+ classifyProvenance,
759
1044
  synthesizeGene,
1045
+ distillWithLLM,
760
1046
  inferCategory,
761
1047
  detectForgery,
762
1048
  assembleCapsule,