@evomap/evolver-core 2.0.0-beta.0 → 2.0.0-beta.2

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 (75) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +17 -11
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +132 -0
  7. package/dist/algo/cycleEngine.js +2 -0
  8. package/dist/algo/cycleFailureClassifier.js +13 -5
  9. package/dist/algo/geneIntake.d.ts +14 -4
  10. package/dist/algo/geneIntake.js +37 -9
  11. package/dist/algo/geneSelection.d.ts +6 -0
  12. package/dist/algo/geneSelection.js +19 -11
  13. package/dist/algo/index.d.ts +1 -0
  14. package/dist/algo/index.js +1 -0
  15. package/dist/assetstore/assetSyncLedger.d.ts +28 -0
  16. package/dist/assetstore/assetSyncLedger.js +86 -0
  17. package/dist/assetstore/index.d.ts +3 -1
  18. package/dist/assetstore/index.js +3 -1
  19. package/dist/assetstore/localJsonl.d.ts +9 -2
  20. package/dist/assetstore/localJsonl.js +75 -23
  21. package/dist/assetstore/pendingSignals.js +3 -1
  22. package/dist/assetstore/provenance.d.ts +7 -2
  23. package/dist/assetstore/provenance.js +85 -25
  24. package/dist/assetstore/reviewFilter.js +2 -1
  25. package/dist/assetstore/reviewLedger.d.ts +6 -0
  26. package/dist/assetstore/reviewLedger.js +9 -0
  27. package/dist/assetstore/seedGenes.d.ts +3 -0
  28. package/dist/assetstore/seedGenes.js +134 -0
  29. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  30. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  31. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  32. package/dist/benchmark/antiGeneImpact.js +34 -0
  33. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  34. package/dist/benchmark/antiGeneRollout.js +13 -1
  35. package/dist/events/eventArchive.d.ts +65 -0
  36. package/dist/events/eventArchive.js +343 -0
  37. package/dist/events/eventStore.js +2 -12
  38. package/dist/events/public.d.ts +3 -1
  39. package/dist/events/public.js +2 -1
  40. package/dist/events/retention.d.ts +24 -1
  41. package/dist/events/retention.js +133 -37
  42. package/dist/exec/autoExec.js +3 -0
  43. package/dist/exec/claudeBridge.js +23 -0
  44. package/dist/exec/runnerRegistry.d.ts +8 -6
  45. package/dist/exec/runnerRegistry.js +71 -18
  46. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  47. package/dist/exec/selfPrObfuscation.js +19 -6
  48. package/dist/hub/agentDirectory.d.ts +90 -0
  49. package/dist/hub/agentDirectory.js +104 -0
  50. package/dist/hub/bindings.js +13 -3
  51. package/dist/hub/capability.d.ts +14 -2
  52. package/dist/hub/fake.d.ts +4 -2
  53. package/dist/hub/fake.js +3 -2
  54. package/dist/hub/index.d.ts +1 -0
  55. package/dist/hub/index.js +1 -0
  56. package/dist/mailbox/catalog.js +3 -0
  57. package/dist/mailbox/ipcServer.js +2 -2
  58. package/dist/mailbox/store.d.ts +14 -0
  59. package/dist/mailbox/store.js +58 -6
  60. package/dist/material/consumer.js +9 -6
  61. package/dist/material/index.d.ts +1 -0
  62. package/dist/material/index.js +1 -0
  63. package/dist/material/materialArchive.d.ts +81 -0
  64. package/dist/material/materialArchive.js +466 -0
  65. package/dist/material/materialStore.d.ts +1 -0
  66. package/dist/material/materialStore.js +6 -13
  67. package/dist/schema/common.d.ts +1 -1
  68. package/dist/schema/common.js +1 -1
  69. package/dist/schema/material.d.ts +5 -5
  70. package/dist/trace/trajectoryExport.js +2 -2
  71. package/dist/wire/geneHints.d.ts +42 -1
  72. package/dist/wire/geneHints.js +52 -1
  73. package/dist/wire/index.d.ts +14 -2
  74. package/dist/wire/index.js +1 -1
  75. package/package.json +2 -1
@@ -0,0 +1,5 @@
1
+ {"type":"Gene","schema_version":"1.12.1","id":"gene_seed_publish_feishu_doc","category":"innovate","signals_match":["publish_markdown_to_feishu","create_feishu_doc","export_report_to_feishu","publish_results_to_lark_doc","feishu_doc","lark_doc"],"preconditions":["lark-cli is installed and available on PATH.","lark-cli auth status reports a ready user or bot identity."],"strategy":["Run lark-cli doctor and require an ok result before publishing.","Use the Docs v2 API and avoid deprecated Docs v1 flags.","Write long Markdown content to a temporary file and pass it as an @file input.","Create or update the document with the intended user or bot identity, then return the document URL.","Never overwrite local lark-cli credential files while preparing the document."],"constraints":{"max_files":2,"forbidden_paths":[".git","node_modules","~/.lark-cli/config.json"]},"validation":["node --version"],"summary":"Publish Markdown content as a Feishu/Lark document through the official CLI, using file-backed content and Docs v2.","asset_id":"sha256:4cdcf2b2a30bcb29a3310360c3cb16c7fc7d427d23a43c836ffa5076cfe3c041"}
2
+ {"type":"Gene","schema_version":"1.12.1","id":"gene_seed_conventional_git_commit","category":"optimize","signals_match":["git_commit","create_commit","commit_changes","conventional_commit","stage_and_commit","write_commit_message"],"preconditions":["A git repository has staged or unstaged changes that should become one logical commit."],"strategy":["Inspect git status plus the staged or unstaged diff before choosing a commit shape.","Pick a Conventional Commits type and optional scope from the actual changed files.","Stage only the intended logical files and never stage credentials or unrelated changes.","Write a present-tense imperative subject under 72 characters, with body or footer when needed.","Do not amend, force-push, hard-reset, or bypass hooks without explicit operator approval."],"constraints":{"max_files":50,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Create a focused Conventional Commits-style git commit from the real diff while avoiding secrets and unrelated changes.","asset_id":"sha256:8f0acd8af3d0a94d6f50e7fe20d96ff39dc982022b851b204eddce47e177f467"}
3
+ {"type":"Gene","schema_version":"1.12.1","id":"gene_seed_poll_bugbot_review","category":"optimize","signals_match":["poll_bugbot","bugbot_review","wait_for_ci_review","pr_review_gate","cursor_bugbot","pr_opened"],"preconditions":["An open GitHub PR exists and Cursor Bugbot is expected to review it."],"strategy":["Poll the Cursor Bugbot check by name until it reaches a terminal conclusion or the operator timeout is reached.","Treat SUCCESS as mergeable only when required checks are green and there are no unresolved Bugbot comments.","Treat NEUTRAL as not passed; fetch and summarize Cursor Bugbot inline comments for the operator.","On FAILURE or ACTION_REQUIRED, surface the findings and do not merge automatically.","Only squash-merge when the operator explicitly asked for merge and the review gate is clean."],"constraints":{"max_files":1,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Wait for Cursor Bugbot on a GitHub PR and gate merge decisions on the actual check conclusion and comments.","asset_id":"sha256:87737bf9d2702667c31e336fc79096f4da7f12a84baae25e827ad62b4a08d332"}
4
+ {"type":"Gene","schema_version":"1.12.1","id":"gene_seed_gateway_timeout_recovery","category":"repair","signals_match":["gateway_timeout","upstream_timeout","http_524","http_504","request_timed_out","retry_on_timeout"],"preconditions":["A tool call, fetch, subagent request, or long command returned a gateway-class timeout."],"strategy":["Treat gateway timeout as transient or size-driven until one bounded recovery attempt proves otherwise.","Retry the exact same operation once, then stop retrying the monolithic call.","If the retry also times out, split the work by file, directory, endpoint, record, section, or time window.","Run independent slices in parallel when safe and merge the results.","Apply the same decomposition recursively only to the slice that still times out."],"constraints":{"max_files":1,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Recover from gateway or upstream timeouts by retrying once, then decomposing the work instead of looping the same large call.","asset_id":"sha256:fe6820477e47a8eb848ecfaa33cdb4242d9af77b78fac21fa1d716244984615c"}
5
+ {"type":"Gene","schema_version":"1.12.1","id":"gene_seed_github_webhook_listener","category":"innovate","signals_match":["github_webhook_listener","bugbot_webhook","passive_pr_notifications","notify_when_bugbot_finishes","webhook_tunnel","github_pr_inbox"],"preconditions":["A developer machine can run a local listener and an outbound tunnel for GitHub webhook delivery."],"strategy":["Use an HMAC-validated local listener for GitHub webhook payloads and deduplicate delivery ids.","Expose the listener through an outbound-only tunnel rather than an inbound public port.","Write vetted notifications into a local inbox with restrictive permissions; never execute webhook payloads.","On session start, re-fetch PR state through GitHub before surfacing any inbox item.","Rotate webhook secrets periodically and immediately after suspected exposure."],"constraints":{"max_files":20,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Deploy a local HMAC-validated GitHub webhook listener that turns PR and Bugbot events into safe operator notifications.","asset_id":"sha256:a64e8d0a91cebd54290451527987230572d8a5bfe90b6ca73cd3368fd3d1f689"}
@@ -2,7 +2,7 @@ import { aggregateLearningHistory } from '../assetstore/learningHistory.js';
2
2
  import { reuseSentiment } from '../ops/reuseOutcomes.js';
3
3
  import { tagOverlapScore } from '../signals/expand.js';
4
4
  import { bannedGenesFromFailures } from './bans.js';
5
- import { isDistilledGeneId } from './geneIntake.js';
5
+ import { geneGenerationSource } from './geneIntake.js';
6
6
  function asStrings(v) {
7
7
  return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
8
8
  }
@@ -18,10 +18,10 @@ function candidateIds(geneId, assetId) {
18
18
  function isBannedCandidate(banned, geneId, assetId) {
19
19
  return candidateIds(geneId, assetId).some((id) => banned.has(id));
20
20
  }
21
- function passesInjectedCandidateGates(candidate, opts) {
21
+ function passesInjectedCandidateGates(candidate, opts, provenance) {
22
22
  const assetId = candidate.assetId;
23
23
  if (opts.provenance && !opts.includeUntrusted) {
24
- if (!assetId || opts.provenance.get(assetId)?.trusted !== true)
24
+ if (!assetId || provenance?.get(assetId)?.trusted !== true)
25
25
  return false;
26
26
  }
27
27
  if (opts.review) {
@@ -79,13 +79,14 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
79
79
  const genes = await store.list('Gene', limit);
80
80
  const banned = await computeBans(store, signals, limit);
81
81
  const sigSet = new Set(signals);
82
+ const provenance = opts.provenance?.snapshot();
82
83
  const out = [];
83
84
  const distilledFallback = [];
84
85
  const antiWarnings = [];
85
86
  for (const g of genes) {
86
87
  // Trust-first (#30): untrusted (e.g. hub-ingested) genes are excluded from the candidate pool by default;
87
88
  // they enter only with includeUntrusted or after an explicit promotion. Provenance is keyed by asset_id.
88
- if (opts.provenance && !opts.includeUntrusted && !opts.provenance.isTrusted(String(g.asset_id)))
89
+ if (opts.provenance && !opts.includeUntrusted && provenance?.get(String(g.asset_id))?.trusted === false)
89
90
  continue;
90
91
  // Review-first (#89/#91): auto-distilled drafts land quarantined until a human approves; rejected drafts stay
91
92
  // out too. No record → eligible (cycle/migrate genes). Symmetric to the provenance trust-first filter above.
@@ -104,18 +105,20 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
104
105
  const literal = signalsMatch.some((m) => sigSet.has(m));
105
106
  const relevant = literal || tagOverlapScore(signals, { signalsMatch, geneId, category, summary }) > 0;
106
107
  if (!relevant) {
107
- // #97: a trusted, approved, non-banned distilled gene that doesn't match the live signals is not a normal
108
- // candidate, but it IS eligible as a last-resort fallback (selection uses it only when nothing clears the
109
- // floor). NB in v2 "distilled" = any intaken gene (intakeGene tags them all gene_distilled_), not just skill-
110
- // derived as in v1 eligibility here is gated by trust/review/ban, not by skill provenance.
108
+ // #97: a trusted, approved, non-banned distilled (or evolved) gene that doesn't match the live signals is not
109
+ // a normal candidate, but it IS eligible as a last-resort fallback (selection uses it only when nothing clears
110
+ // the floor). Eligibility here is gated by trust/review/ban, not by skill provenance. Provenance is read from
111
+ // generation_meta (V1 #302); a legacy gene without it falls back to the `gene_distilled_` id namespace.
111
112
  // Lightweight: no learning-history aggregation — it is picked first-match, not ranked by health.
112
- if (isDistilledGeneId(geneId)) {
113
+ const gsrc = geneGenerationSource(g, geneId);
114
+ if (gsrc === 'distilled' || gsrc === 'evolved') {
113
115
  distilledFallback.push({
114
116
  geneId,
115
117
  assetId,
116
118
  signalsMatch,
117
119
  view: emptyLearningView(geneId),
118
120
  reuseCount: 0,
121
+ generationSource: gsrc,
119
122
  ...(category ? { category } : {}),
120
123
  ...(summary ? { summary } : {}),
121
124
  });
@@ -145,7 +148,7 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
145
148
  if (opts.hubCandidates && opts.hubCandidates.length > 0) {
146
149
  const localIds = new Set(out.map((c) => c.geneId));
147
150
  for (const h of opts.hubCandidates) {
148
- if (!passesInjectedCandidateGates(h, opts))
151
+ if (!passesInjectedCandidateGates(h, opts, provenance))
149
152
  continue;
150
153
  if (localIds.has(h.geneId))
151
154
  continue; // a trusted local gene already covers this id
@@ -160,7 +163,10 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
160
163
  }
161
164
  }
162
165
  for (const a of await store.list('AntiGene', limit)) {
163
- if (opts.review && !opts.review.isApproved(String(a.asset_id)))
166
+ // AntiGene is negative memory that changes an autonomous prompt. Unlike legacy/cycle-authored Genes, it must
167
+ // never inherit ReviewLedger's backward-compatible "no record = approved" default: no ledger, no record,
168
+ // quarantined, and rejected all fail closed. Only an explicit human approval can enable warning injection.
169
+ if (!opts.review?.isExplicitlyApproved(String(a.asset_id)))
164
170
  continue;
165
171
  const trigger = asStrings(a['trigger']);
166
172
  const avoid = asStrings(a['avoid']);
@@ -1,3 +1,4 @@
1
+ import type { ConversationCapabilityHit } from './conversationSniffer.js';
1
2
  /** The "five-question shape" of a capability proposal (ported verbatim from v1). */
2
3
  export interface FiveQuestionShape {
3
4
  title: string;
@@ -30,6 +31,7 @@ export interface ExtractCandidatesInput {
30
31
  toolCalls?: readonly string[];
31
32
  signals?: readonly string[];
32
33
  failedCapsules?: readonly FailedCapsuleLike[];
34
+ conversationHits?: readonly ConversationCapabilityHit[];
33
35
  }
34
36
  /**
35
37
  * Mine capability candidates from three sources (ported v1): repeated tool calls (>=3), active opportunity
@@ -80,6 +80,11 @@ export function extractCapabilityCandidates(inp) {
80
80
  const title = `Repeated tool usage: ${tool}`;
81
81
  out.push(candidate('transcript', stableHash(title), title, signals, `Observed ${count} occurrences of tool call ${tool}.`, signalTags));
82
82
  }
83
+ // A2. Conversation sniffer hits: a transcript explicitly proved a reusable capability.
84
+ for (const hit of inp.conversationHits ?? []) {
85
+ const hitSignals = [...new Set([...(hit.signals ?? []), ...signals].map((s) => String(s)).filter(Boolean))];
86
+ out.push(candidate('transcript', stableHash('conversation:' + hit.id), hit.title || 'Verified reusable conversation capability', hitSignals, hit.evidence.length > 0 ? hit.evidence.slice(0, 2).join('; ') : hit.summary, [...new Set([...signalTags, ...expandSignals(hitSignals)])]));
87
+ }
83
88
  // B. Signal-mapped candidates: predefined defensive/opportunity signals.
84
89
  for (const sc of SIGNAL_CANDIDATES) {
85
90
  if (!signals.some((s) => s === sc.signal || s.startsWith(sc.signal + ':')))
@@ -0,0 +1,18 @@
1
+ import type { SignalSourceTurn } from '../signals/extractor.js';
2
+ export interface ConversationSnifferTurn extends SignalSourceTurn {
3
+ role?: string;
4
+ }
5
+ export interface ConversationCapabilityHit {
6
+ id: string;
7
+ title: string;
8
+ summary: string;
9
+ signals: string[];
10
+ evidence: string[];
11
+ score: number;
12
+ reasons: string[];
13
+ toolCalls: string[];
14
+ }
15
+ export interface ConversationSnifferOptions {
16
+ maxHits?: number;
17
+ }
18
+ export declare function sniffConversationCapabilities(turns: readonly ConversationSnifferTurn[], opts?: ConversationSnifferOptions): ConversationCapabilityHit[];
@@ -0,0 +1,132 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { redactString } from '../hub/sanitize.js';
3
+ const REUSABLE_RE = /\b(reusable|repeatable|workflow|playbook|runbook|capability|procedure|pattern|recipe|documented|future runs?|next time|can reuse|reuse this)\b|可复用|复用|能力|流程|工作流/i;
4
+ const PROOF_RE = /\b(validated|verified|passed|green|success(?:ful|fully)?|succeeded|works?|completed|published|uploaded|recorded:true|exit code:?\s*0|all tests passed)\b|验证|通过|成功|已发布|可用/i;
5
+ const FAILURE_RE = /\b(failed|failure|error|exception|traceback|exit code:?\s*[1-9]|not working|unable to)\b|失败|错误|报错/i;
6
+ const EXIT_ZERO_RE = /\bexit code:?\s*0\b/i;
7
+ const EXIT_NON_ZERO_RE = /\bexit code:?\s*[1-9]\d*\b/i;
8
+ const DOMAIN_TOKENS = [
9
+ [/\b(lark|feishu|飞书)\b/i, 'lark'],
10
+ [/\b(github|gh|pull request|bugbot)\b/i, 'github'],
11
+ [/\b(playwright|screenshot|visual)\b/i, 'playwright'],
12
+ [/\b(vitest|jest|test)\b/i, 'test'],
13
+ [/\b(pnpm|npm|yarn)\b/i, 'package-manager'],
14
+ [/\b(hub|publish|marketplace)\b/i, 'hub-publish'],
15
+ [/\b(release|checksum|sha256)\b/i, 'release'],
16
+ [/\b(api|http|endpoint)\b/i, 'api'],
17
+ [/\b(document|docs?|markdown)\b/i, 'docs'],
18
+ ];
19
+ function cleanText(value, max = 220) {
20
+ const text = redactString(String(value ?? '').replace(/\s+/g, ' ').trim());
21
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
22
+ }
23
+ function turnText(turn) {
24
+ return cleanText([turn.text, turn.toolResult, turn.errorMessage].filter(Boolean).join('\n'), 500);
25
+ }
26
+ function stableId(parts) {
27
+ return `conversation_capability_${createHash('sha1').update(JSON.stringify(parts), 'utf8').digest('hex').slice(0, 12)}`;
28
+ }
29
+ function normalizeSignal(value) {
30
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48);
31
+ }
32
+ function collectToolCalls(turns) {
33
+ const seen = new Set();
34
+ for (const turn of turns) {
35
+ const tool = cleanText(turn.toolName ?? '', 80);
36
+ if (tool)
37
+ seen.add(tool);
38
+ }
39
+ return [...seen].slice(0, 8);
40
+ }
41
+ function domainSignals(text, toolCalls) {
42
+ const out = new Set();
43
+ for (const [re, signal] of DOMAIN_TOKENS) {
44
+ if (re.test(text))
45
+ out.add(signal);
46
+ }
47
+ for (const tool of toolCalls) {
48
+ const normalized = normalizeSignal(tool);
49
+ if (normalized)
50
+ out.add(normalized);
51
+ }
52
+ return [...out].slice(0, 6);
53
+ }
54
+ function isToolTurn(turn) {
55
+ return turn.role === 'tool' || Boolean(turn.toolName);
56
+ }
57
+ function classifyToolOutcome(turn) {
58
+ const text = turnText(turn);
59
+ if (EXIT_NON_ZERO_RE.test(text))
60
+ return 'failure';
61
+ if (EXIT_ZERO_RE.test(text))
62
+ return 'success';
63
+ if (PROOF_RE.test(text) && !FAILURE_RE.test(text))
64
+ return 'success';
65
+ if (FAILURE_RE.test(text))
66
+ return 'failure';
67
+ return 'unknown';
68
+ }
69
+ function hasTerminalSuccessfulToolRun(turns) {
70
+ let lastDecisive = 'unknown';
71
+ for (const turn of turns) {
72
+ if (!isToolTurn(turn))
73
+ continue;
74
+ const outcome = classifyToolOutcome(turn);
75
+ if (outcome !== 'unknown')
76
+ lastDecisive = outcome;
77
+ }
78
+ return lastDecisive === 'success';
79
+ }
80
+ function pickEvidence(turns, matcher, max = 3) {
81
+ const evidence = [];
82
+ for (const turn of turns) {
83
+ const text = turnText(turn);
84
+ if (!text || !matcher.test(text))
85
+ continue;
86
+ evidence.push(cleanText(text, 180));
87
+ if (evidence.length >= max)
88
+ break;
89
+ }
90
+ return evidence;
91
+ }
92
+ function pickSummary(turns, reusableEvidence, proofEvidence) {
93
+ const assistant = turns
94
+ .filter((turn) => turn.role === 'assistant' && !turn.isMeta)
95
+ .map(turnText)
96
+ .find((text) => text.length >= 40 && (REUSABLE_RE.test(text) || PROOF_RE.test(text)));
97
+ return cleanText(assistant ?? reusableEvidence[0] ?? proofEvidence[0] ?? 'Verified reusable conversation capability.', 180);
98
+ }
99
+ export function sniffConversationCapabilities(turns, opts = {}) {
100
+ const usableTurns = turns.filter((turn) => !turn.isMeta);
101
+ if (usableTurns.length === 0)
102
+ return [];
103
+ const allText = usableTurns.map(turnText).filter(Boolean).join('\n');
104
+ if (!allText.trim())
105
+ return [];
106
+ const reusableEvidence = pickEvidence(usableTurns, REUSABLE_RE);
107
+ const proofEvidence = pickEvidence(usableTurns, PROOF_RE);
108
+ if (reusableEvidence.length === 0 || proofEvidence.length === 0)
109
+ return [];
110
+ const failureOnly = FAILURE_RE.test(allText) && !PROOF_RE.test(allText);
111
+ if (failureOnly)
112
+ return [];
113
+ if (!hasTerminalSuccessfulToolRun(usableTurns))
114
+ return [];
115
+ const toolCalls = collectToolCalls(usableTurns);
116
+ const domain = domainSignals(allText, toolCalls);
117
+ const signals = [...new Set(['reusable_capability', 'verified_workflow', ...domain])].slice(0, 8);
118
+ const score = 4 + Math.min(2, reusableEvidence.length) + Math.min(2, proofEvidence.length) + Math.min(2, toolCalls.length);
119
+ const summary = pickSummary(usableTurns, reusableEvidence, proofEvidence);
120
+ const titleSignal = domain[0] ?? 'verified workflow';
121
+ const hit = {
122
+ id: stableId({ summary, signals, evidence: [...reusableEvidence, ...proofEvidence].slice(0, 4) }),
123
+ title: `Verified reusable capability: ${titleSignal}`,
124
+ summary,
125
+ signals,
126
+ evidence: [...reusableEvidence, ...proofEvidence].slice(0, 4),
127
+ score,
128
+ reasons: ['reusable_marker', 'proof_marker', ...(toolCalls.length > 0 ? ['tool_evidence'] : [])],
129
+ toolCalls,
130
+ };
131
+ return [hit].slice(0, opts.maxHits ?? 1);
132
+ }
@@ -183,6 +183,8 @@ export class CycleEngine {
183
183
  // Locality must recognise V2's real gene prefix `gene_distilled_` (isDistilledGeneId), not just the
184
184
  // V1-only `gene_auto_` / content-hash `sha256:` forms — otherwise this gate stays shut for every real V2
185
185
  // gene and classifyCycleFailure never runs (mirror of isLocalGeneratedGene in cycleFailureClassifier.ts).
186
+ // NB the prefix is now a NAMESPACE marker, not a provenance tag (the authoritative source is generation_meta,
187
+ // V1 #302); this call site only has a gene id, so it falls back to the prefix.
186
188
  const localCurrentGene = current?.geneId !== undefined && (isDistilledGeneId(current.geneId) || /^sha256:/.test(current.geneId) || /^gene_auto_/.test(current.geneId));
187
189
  // Run classification when we have ANY host context (an exec transcript or an injected failureContext) OR a
188
190
  // local-gene blast to judge; otherwise stay default-open (unclassified, no payload).
@@ -12,13 +12,17 @@
12
12
  import { isDistilledGeneId } from './geneIntake.js';
13
13
  // "[NO SESSION LOGS FOUND]" et al. — the host gave evolver nothing to evolve from.
14
14
  const HOST_NO_TRANSCRIPT_RE = /\[no session logs? found\]|no session log|no transcript/i;
15
+ // Interactive runner gates can stop execution before the provider is invoked. They still belong to the
16
+ // host_provider_error compatibility bucket because the local gene never had a chance to run.
17
+ const HOST_RUNNER_ERR_RE = /Workspace Trust Required/i;
15
18
  // Host LLM-provider errors. None of these strings are emitted by evolver core (v1 #279 verified the same;
16
19
  // keeping the regex byte-for-byte equivalent), so their presence means the host Agent's provider call failed.
17
- const HOST_PROVIDER_ERR_RE = /\bLLM ERROR\b|\bMaxTokens\b|field MaxTokens|max_tokens[^\n]{0,24}invalid|insufficient_quota|invalid_api_key|\brate limit(?:ed| exceeded)?\b|quota exceeded|context length exceeded|maximum context length/i;
20
+ const HOST_PROVIDER_ERR_RE = /\bLLM ERROR\b|\bMaxTokens\b|field MaxTokens|max_tokens[^\n]{0,24}invalid|insufficient_quota|invalid_api_key|\brate limit(?:ed| exceeded)?\b|quota exceeded|context length exceeded|maximum context length|disabled Claude subscription access|Use an Anthropic API key|not authenticated|authentication required|login required|not logged in/i;
18
21
  // Locality prefixes a "locally-generated" gene id can carry:
19
- // - `gene_distilled_` — the ONLY prefix V2 actually mints (geneIntake.DISTILLED_ID_PREFIX,
20
- // surfaced via isDistilledGeneId). Without this the whole `local_gene_no_blast` bucket never
21
- // fired for real V2 genes (they all carry this prefix). Reuse the single-source-of-truth helper.
22
+ // - `gene_distilled_` — the namespace V2 mints for every intaken gene (geneIntake.DISTILLED_ID_PREFIX,
23
+ // surfaced via isDistilledGeneId). NB this is a NAMESPACE marker now, not a provenance tag: the authoritative
24
+ // source is generation_meta (V1 #302, read via geneGenerationSource), but this call site only has a gene id, so
25
+ // it falls back to the prefix. Without this the whole `local_gene_no_blast` bucket never fired for real V2 genes.
22
26
  // - `gene_auto_` — V1-only legacy form (V2 never produces it); kept so V1-shaped inputs still match.
23
27
  // - `sha256:` — a content asset_id (not a logical gene id); kept for back-compat / loose callers.
24
28
  function isLocalGeneratedGene(geneId) {
@@ -60,7 +64,11 @@ export function classifyCycleFailure(opts) {
60
64
  if (hasSessionLog && (!log.trim() || HOST_NO_TRANSCRIPT_RE.test(log))) {
61
65
  return { failureClass: 'host_no_transcript', reason: 'no session transcript to evolve from' };
62
66
  }
63
- // (b) Host LLM-provider error surfaced in the transcript.
67
+ // (b) Host runner preflight or LLM-provider error surfaced in the transcript.
68
+ const runner = hasSessionLog ? log.match(HOST_RUNNER_ERR_RE) : null;
69
+ if (runner) {
70
+ return { failureClass: 'host_provider_error', reason: `host runner error: ${String(runner[0]).slice(0, 60)}` };
71
+ }
64
72
  const prov = hasSessionLog ? log.match(HOST_PROVIDER_ERR_RE) : null;
65
73
  if (prov) {
66
74
  return { failureClass: 'host_provider_error', reason: `host LLM-provider error: ${String(prov[0]).slice(0, 60)}` };
@@ -1,10 +1,19 @@
1
- import { type Gene } from '../wire/index.js';
1
+ import { type Gene, type GenerationSource } from '../wire/index.js';
2
2
  /**
3
- * True when a gene id is a distilled (skill-derived) gene. Single source of truth for the prefix check also
4
- * consumed by candidate assembly / selection to recognise the broadly-applicable distilled genes that may be
5
- * reused as a last-resort fallback when no gene matches the live signals (ported from v1 #97).
3
+ * True when a gene id is in the `gene_distilled_` namespace. NB this prefix is now a NAMESPACE marker, NOT a
4
+ * provenance tag: v1 used it to mean "skill-derived" (auto-evolved genes were `gene_auto_`), but v2's intakeGene
5
+ * tags EVERY intaken gene this way regardless of origin. The authoritative provenance is {@link geneGenerationSource};
6
+ * this prefix check remains as a back-compat fallback for legacy pooled genes that predate generation_meta, and for
7
+ * call sites that only have a gene id (not the full record).
6
8
  */
7
9
  export declare function isDistilledGeneId(id: string | undefined | null): boolean;
10
+ /**
11
+ * The authoritative gene-generation source: read `generation_meta.source` from a gene-shaped record (V1 #302). Falls
12
+ * back to the id-prefix namespace when the record carries no generation_meta (legacy pooled genes predate the field),
13
+ * returning 'distilled' for a `gene_distilled_` id and 'manual' otherwise — so the fallback never claims a higher
14
+ * provenance tier than the prefix can vouch for. `null` means "undetermined" (no record, no recognizable id).
15
+ */
16
+ export declare function geneGenerationSource(gene: Record<string, unknown> | null | undefined, geneId?: string | null): GenerationSource | null;
8
17
  export interface GeneCandidate {
9
18
  id?: string;
10
19
  category?: string;
@@ -19,6 +28,7 @@ export interface GeneCandidate {
19
28
  validation?: readonly string[];
20
29
  routing_hint?: unknown;
21
30
  tool_policy?: unknown;
31
+ generation_meta?: unknown;
22
32
  }
23
33
  /** Minimal shape of an existing gene needed for dedup. */
24
34
  export interface ExistingGeneRef {
@@ -5,20 +5,44 @@
5
5
  // signals fully overlap an existing one (dedup, so the pool does not grow redundant). The gene's CONTENT
6
6
  // is generated upstream (the agent runtime / a distillation prompt — generative, out of core scope); this
7
7
  // slice is the structural gate. Pairs with capabilityCandidates (which proposes what to distill).
8
- import { computeAssetId, validateWire, SCHEMA_VERSION, normalizeRoutingHint, normalizeToolPolicy, stripGeneHints, } from '../wire/index.js';
8
+ import { computeAssetId, validateWire, SCHEMA_VERSION, normalizeRoutingHint, normalizeToolPolicy, normalizeGenerationMeta, stripGeneHints, } from '../wire/index.js';
9
9
  const VALID_CATEGORIES = ['repair', 'optimize', 'innovate', 'explore'];
10
10
  const DEFAULT_FORBIDDEN_PATHS = ['.git', 'node_modules'];
11
11
  const DEFAULT_MAX_FILES = 12;
12
12
  /** Id prefix every distilled (skill-derived) gene carries. Recognised via {@link isDistilledGeneId}. */
13
13
  const DISTILLED_ID_PREFIX = 'gene_distilled_';
14
14
  /**
15
- * True when a gene id is a distilled (skill-derived) gene. Single source of truth for the prefix check also
16
- * consumed by candidate assembly / selection to recognise the broadly-applicable distilled genes that may be
17
- * reused as a last-resort fallback when no gene matches the live signals (ported from v1 #97).
15
+ * True when a gene id is in the `gene_distilled_` namespace. NB this prefix is now a NAMESPACE marker, NOT a
16
+ * provenance tag: v1 used it to mean "skill-derived" (auto-evolved genes were `gene_auto_`), but v2's intakeGene
17
+ * tags EVERY intaken gene this way regardless of origin. The authoritative provenance is {@link geneGenerationSource};
18
+ * this prefix check remains as a back-compat fallback for legacy pooled genes that predate generation_meta, and for
19
+ * call sites that only have a gene id (not the full record).
18
20
  */
19
21
  export function isDistilledGeneId(id) {
20
22
  return typeof id === 'string' && id.startsWith(DISTILLED_ID_PREFIX);
21
23
  }
24
+ /**
25
+ * The authoritative gene-generation source: read `generation_meta.source` from a gene-shaped record (V1 #302). Falls
26
+ * back to the id-prefix namespace when the record carries no generation_meta (legacy pooled genes predate the field),
27
+ * returning 'distilled' for a `gene_distilled_` id and 'manual' otherwise — so the fallback never claims a higher
28
+ * provenance tier than the prefix can vouch for. `null` means "undetermined" (no record, no recognizable id).
29
+ */
30
+ export function geneGenerationSource(gene, geneId) {
31
+ const meta = gene?.['generation_meta'];
32
+ if (meta && typeof meta === 'object') {
33
+ const src = meta['source'];
34
+ if (typeof src === 'string' && (src === 'evolved' || src === 'distilled' || src === 'manual'))
35
+ return src;
36
+ }
37
+ // Legacy fallback: a `gene_distilled_` id is at best a distilled gene (v2 minted it for ALL intake sources, so it
38
+ // cannot be trusted as "evolved"); any other id is treated as manual (human-authored / externally seeded).
39
+ const id = geneId ?? (typeof gene?.['id'] === 'string' ? String(gene['id']) : null);
40
+ if (id && isDistilledGeneId(id))
41
+ return 'distilled';
42
+ if (id)
43
+ return 'manual';
44
+ return null;
45
+ }
22
46
  function fnv1a(s) {
23
47
  let h = 2166136261;
24
48
  for (let i = 0; i < s.length; i++) {
@@ -65,6 +89,9 @@ export function intakeGene(candidate, existing = []) {
65
89
  // absent when malformed/empty. The router / tool-gate reads absent === "no opinion".
66
90
  const routingHint = normalizeRoutingHint(candidate.routing_hint);
67
91
  const toolPolicy = normalizeToolPolicy(candidate.tool_policy);
92
+ // v2-delta provenance + quality metadata (v1 #302): normalized so a candidate carrying it lands canonical,
93
+ // dropped to absent when malformed/no-recognized-source.
94
+ const generationMeta = normalizeGenerationMeta(candidate.generation_meta);
68
95
  const gene = {
69
96
  type: 'Gene',
70
97
  schema_version: SCHEMA_VERSION,
@@ -81,16 +108,17 @@ export function intakeGene(candidate, existing = []) {
81
108
  ...(candidate.preconditions ? { preconditions: [...candidate.preconditions] } : {}),
82
109
  ...(routingHint ? { routing_hint: routingHint } : {}),
83
110
  ...(toolPolicy ? { tool_policy: toolPolicy } : {}),
111
+ ...(generationMeta ? { generation_meta: generationMeta } : {}),
84
112
  asset_id: '',
85
113
  };
86
- // asset_id folds in the hints (intake's own canonical shape — gep-sdk canonicalize hashes every own key).
87
- // NB: this is NOT byte-equal to what v1's createGene would hash for the same logical gene — v1 always emits
114
+ // asset_id folds in the hints + generation_meta (intake's own canonical shape — gep-sdk canonicalize hashes every
115
+ // own key). NB: this is NOT byte-equal to what v1's createGene would hash for the same logical gene — v1 always emits
88
116
  // routing_hint/tool_policy:null plus empty epigenetic_marks/learning_history/anti_patterns/preconditions that
89
117
  // v2 intake omits, so the canonical shapes differ. Self-consistent here; not a cross-impl parity guarantee.
90
118
  gene.asset_id = computeAssetId(gene) ?? '';
91
- // Structural gate validates the gep-sdk-known CORE. routing_hint / tool_policy are v2-delta (pending a
92
- // gene-schema bump), so strip them before the check — else additionalProperties:false rejects the gene at
93
- // intake. The full hinted gene stays schema-invalid until the SDK catches up — surfaced by the advisory
119
+ // Structural gate validates the gep-sdk-known CORE. routing_hint / tool_policy / generation_meta are v2-delta
120
+ // (pending a gene-schema bump), so strip them before the check — else additionalProperties:false rejects the
121
+ // gene at intake. The full hinted gene stays schema-invalid until the SDK catches up — surfaced by the advisory
94
122
  // validateWire preview (evolver_gep_build) and enforced by the hub on receipt, NOT by a local egress gate
95
123
  // (the publish/egress path is sanitize-only). Same v2-delta contract Capsule's proof_of_work had pre-1.11.0.
96
124
  const v = validateWire(stripGeneHints(gene));
@@ -4,6 +4,7 @@ import { type GeneHealth } from './geneHealth.js';
4
4
  import type { GeneLearningView } from '../assetstore/learningHistory.js';
5
5
  import type { AssetRecord } from '../assetstore/provider.js';
6
6
  import { type ExplorationInput } from './exploration.js';
7
+ import type { GenerationSource } from '../wire/index.js';
7
8
  /** 一个候选 gene 的选择期素材. */
8
9
  export interface GeneCandidateInput {
9
10
  geneId: string;
@@ -43,6 +44,11 @@ export interface GeneCandidateInput {
43
44
  * effect, so the factor is dormant until a caller injects the signal (default-off).
44
45
  */
45
46
  reuseAdjust?: number;
47
+ /**
48
+ * Authoritative provenance tag from Gene.generation_meta.source. The legacy `gene_distilled_` prefix remains a
49
+ * fallback only for old candidates that do not carry this field.
50
+ */
51
+ generationSource?: GenerationSource;
46
52
  }
47
53
  export interface SelectionInput {
48
54
  signals: readonly string[];
@@ -88,6 +88,14 @@ function matchesScoredIdentity(a, b) {
88
88
  function withoutCandidateIdentity(scored, c) {
89
89
  return scored.filter((s) => !matchesCandidateIdentity(s, c));
90
90
  }
91
+ function isReusableGenerationSource(source) {
92
+ return source === 'distilled' || source === 'evolved';
93
+ }
94
+ function isReusableFallbackCandidate(c) {
95
+ if (c.generationSource !== undefined)
96
+ return isReusableGenerationSource(c.generationSource);
97
+ return isDistilledGeneId(c.geneId);
98
+ }
91
99
  function forcedSelection(input, scored, scoreForcedFallback, floor) {
92
100
  const forcedGeneId = input.forcedGeneId?.trim();
93
101
  if (!forcedGeneId)
@@ -116,7 +124,7 @@ function forcedSelection(input, scored, scoreForcedFallback, floor) {
116
124
  forcedFallback.reasons.push('forced gene rejected: epigenetic suppression');
117
125
  return { scoredForChoice: scoredCandidatesOnly, forceRejected: true };
118
126
  }
119
- if (!isDistilledGeneId(fallback.geneId)) {
127
+ if (!isReusableFallbackCandidate(fallback)) {
120
128
  forcedFallback.reasons.push('forced gene rejected: suppressed fallback');
121
129
  return { scoredForChoice: scoredCandidatesOnly, forceRejected: true };
122
130
  }
@@ -141,15 +149,15 @@ export const engineHealthSelection = {
141
149
  let selectedAssetId = forced.selectedAssetId ?? chosen?.assetId;
142
150
  if (!selectedGeneId) {
143
151
  // Issue #97 (ported from v1 selector.js): no candidate matched any live signal (every gene scored <= 0). Rather
144
- // than fall straight through to a blind innovate, reuse a *distilled* gene if one is available. NB the prefix
145
- // does NOT mean "skill-derived" in v2 the way it did in v1: v1 tagged only skill genes gene_distilled_ (auto-
146
- // evolved genes were gene_auto_), but v2's intakeGene tags EVERY intaken gene gene_distilled_ (autoExec even
147
- // intakes task strategies that way) and has no gene_auto_. So this pool is "any trusted/approved/non-suppressed
148
- // pooled gene that does not match", not specifically skill-derived — the safety rests on the upstream gates
149
- // (trust #30 / review #89-91 / signal-ban / inert-ban #195) plus the epigenetic skip below, not on a skill-
150
- // provenance low-blast-radius assumption. We skip any distilled fallback epigenetically suppressed in this env;
151
- // v2's event-log-derived epigeneticPenalty is the analog of v1's asset-mark hard suppression (related band, not
152
- // the identical predicate). No usable distilled fallback → stay null so the caller still innovates (the contract).
152
+ // than fall straight through to a blind innovate, reuse a *distilled* (or evolved) gene if one is available.
153
+ // The fallback pool is built by candidateAssembly, which now filters by generation_meta.source {distilled,
154
+ // evolved} (V1 #302), falling back to the `gene_distilled_` id namespace for legacy genes. The id-prefix check
155
+ // below is a defense-in-depth filter for callers that bypass candidateAssembly; the authoritative provenance
156
+ // lives on generation_meta (see geneGenerationSource). The safety rests on the upstream gates (trust #30 /
157
+ // review #89-91 / signal-ban / inert-ban #195) plus the epigenetic skip below, not on a skill-provenance
158
+ // low-blast-radius assumption. We skip any distilled fallback epigenetically suppressed in this env; v2's
159
+ // event-log-derived epigeneticPenalty is the analog of v1's asset-mark hard suppression (related band, not the
160
+ // identical predicate). No usable distilled fallback → stay null so the caller still innovates (the contract).
153
161
  //
154
162
  // v1 #97 parity: the distilled fallback is a *no-signal-match* last resort — it must fire only when NOTHING
155
163
  // scored above zero, NOT merely when nothing cleared a positive `floor`. Otherwise a real but weak signal match
@@ -157,7 +165,7 @@ export const engineHealthSelection = {
157
165
  // keeps such a match. When candidates scored > 0 but below floor, defer to V2's floor policy (innovate).
158
166
  const noSignalMatch = !forced.forceRejected && forced.scoredForChoice.every((s) => s.score <= 0);
159
167
  const fb = noSignalMatch
160
- ? (input.distilledFallback ?? []).find((c) => isDistilledGeneId(c.geneId) && (c.epigeneticPenalty ?? 0) === 0)
168
+ ? (input.distilledFallback ?? []).find((c) => isReusableFallbackCandidate(c) && (c.epigeneticPenalty ?? 0) === 0)
161
169
  : undefined;
162
170
  if (fb) {
163
171
  const fbScored = scoreCandidate(input.signals, fb);
@@ -3,6 +3,7 @@ export * from './exploration.js';
3
3
  export * from './epigenetics.js';
4
4
  export * from './confidence.js';
5
5
  export * from './capabilityCandidates.js';
6
+ export * from './conversationSniffer.js';
6
7
  export * from './geneIntake.js';
7
8
  export * from './candidateAssembly.js';
8
9
  export * from './genePromotion.js';
@@ -3,6 +3,7 @@ export * from './exploration.js';
3
3
  export * from './epigenetics.js';
4
4
  export * from './confidence.js';
5
5
  export * from './capabilityCandidates.js';
6
+ export * from './conversationSniffer.js';
6
7
  export * from './geneIntake.js';
7
8
  export * from './candidateAssembly.js';
8
9
  export * from './genePromotion.js';
@@ -0,0 +1,28 @@
1
+ import type { AssetKind } from './provider.js';
2
+ export type AssetSyncSource = 'hub';
3
+ export type AssetSyncScope = 'purchased' | 'published';
4
+ export interface AssetSyncRecord {
5
+ assetId: string;
6
+ type: Extract<AssetKind, 'Gene' | 'Capsule'>;
7
+ source: AssetSyncSource;
8
+ scope: AssetSyncScope;
9
+ syncedAt: string;
10
+ remoteAssetId: string;
11
+ logicalId?: string;
12
+ status?: string;
13
+ forced?: true;
14
+ collisionWithAssetId?: string;
15
+ }
16
+ export declare class AssetSyncLedger {
17
+ private readonly now;
18
+ private readonly path;
19
+ private readonly index;
20
+ private loaded;
21
+ constructor(baseDir: string, now?: () => number);
22
+ append(rec: Omit<AssetSyncRecord, 'syncedAt'> & {
23
+ syncedAt?: string;
24
+ }): AssetSyncRecord;
25
+ get(assetId: string): AssetSyncRecord | null;
26
+ list(): AssetSyncRecord[];
27
+ private load;
28
+ }