@evomap/evolver 1.88.1 → 1.88.3

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 (66) hide show
  1. package/index.js +159 -3
  2. package/package.json +2 -1
  3. package/src/adapters/claudeCode.js +21 -1
  4. package/src/adapters/hookAdapter.js +4 -2
  5. package/src/adapters/scripts/_runtimePaths.js +160 -36
  6. package/src/adapters/scripts/evolver-session-start.js +14 -10
  7. package/src/adapters/scripts/evolver-task-recall.js +173 -0
  8. package/src/atp/atpExecute.js +20 -7
  9. package/src/atp/cli.js +17 -9
  10. package/src/atp/protocol.js +41 -0
  11. package/src/config.js +29 -0
  12. package/src/evolve/guards.js +1 -1
  13. package/src/evolve/pipeline/collect.js +1 -1
  14. package/src/evolve/pipeline/dispatch.js +1 -1
  15. package/src/evolve/pipeline/enrich.js +1 -1
  16. package/src/evolve/pipeline/hub.js +1 -1
  17. package/src/evolve/pipeline/select.js +1 -1
  18. package/src/evolve/pipeline/signals.js +1 -1
  19. package/src/evolve/utils.js +1 -1
  20. package/src/evolve.js +1 -1
  21. package/src/forceUpdate.js +108 -3
  22. package/src/gep/a2aProtocol.js +1 -1
  23. package/src/gep/assetCallLog.js +40 -1
  24. package/src/gep/autoDistillConv.js +1 -1
  25. package/src/gep/autoDistillLlm.js +1 -1
  26. package/src/gep/candidateEval.js +1 -1
  27. package/src/gep/candidates.js +1 -1
  28. package/src/gep/contentHash.js +1 -1
  29. package/src/gep/conversationSniffer.js +1 -1
  30. package/src/gep/crypto.js +1 -1
  31. package/src/gep/curriculum.js +1 -1
  32. package/src/gep/deviceId.js +1 -1
  33. package/src/gep/envFingerprint.js +1 -1
  34. package/src/gep/epigenetics.js +1 -1
  35. package/src/gep/execBridge.js +1 -1
  36. package/src/gep/explore.js +1 -1
  37. package/src/gep/hash.js +1 -1
  38. package/src/gep/hubFetch.js +1 -1
  39. package/src/gep/hubReview.js +1 -1
  40. package/src/gep/hubSearch.js +1 -1
  41. package/src/gep/hubVerify.js +1 -1
  42. package/src/gep/learningSignals.js +1 -1
  43. package/src/gep/memoryGraph.js +1 -1
  44. package/src/gep/memoryGraphAdapter.js +1 -1
  45. package/src/gep/mutation.js +1 -1
  46. package/src/gep/narrativeMemory.js +1 -1
  47. package/src/gep/openPRRegistry.js +1 -1
  48. package/src/gep/personality.js +1 -1
  49. package/src/gep/policyCheck.js +1 -1
  50. package/src/gep/prompt.js +1 -1
  51. package/src/gep/recallInject.js +1 -0
  52. package/src/gep/recallVerifier.js +1 -1
  53. package/src/gep/reflection.js +1 -1
  54. package/src/gep/sanitize.js +5 -0
  55. package/src/gep/selector.js +1 -1
  56. package/src/gep/skillDistiller.js +1 -1
  57. package/src/gep/solidify.js +1 -1
  58. package/src/gep/strategy.js +1 -1
  59. package/src/gep/workspaceKeychain.js +1 -1
  60. package/src/proxy/extensions/traceControl.js +1 -0
  61. package/src/proxy/index.js +46 -4
  62. package/src/proxy/inject.js +1 -0
  63. package/src/proxy/lifecycle/manager.js +457 -2
  64. package/src/proxy/mailbox/store.js +1 -0
  65. package/src/proxy/router/messages_route.js +57 -8
  66. package/src/proxy/trace/extractor.js +1 -0
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // evolver-task-recall.js
3
+ // UserPromptSubmit hook: on each user prompt, find GEP assets (Hub genes +
4
+ // local genes) that match the task and inject a distilled hint so a GENERAL
5
+ // agent benefits from prior distilled capabilities without manually calling
6
+ // MCP tools. This is the per-harness shell around src/gep/recallInject.js.
7
+ //
8
+ // Input (stdin JSON, Claude Code UserPromptSubmit):
9
+ // { "prompt": "...", "session_id", "cwd", "transcript_path", ... }
10
+ // Output (stdout JSON, exit 0 ALWAYS):
11
+ // enforce + match -> { agent_message, additionalContext,
12
+ // hookSpecificOutput: { hookEventName, additionalContext } }
13
+ // everything else -> {} (injects nothing)
14
+ //
15
+ // DESIGN CONTRACT (the fail-open core — see also recallInject.js invariants):
16
+ // - DEFAULT off. off finishes {} WITHOUT parsing the prompt body.
17
+ // - shadow computes + logs what WOULD inject but injects nothing.
18
+ // - enforce injects the distilled hint.
19
+ // - FAIL-OPEN: any error/timeout/empty -> exactly one finish({}). The hook
20
+ // blocks the user's prompt, so it must NEVER hang or crash the session.
21
+ // Claude Code's timeout fail-open behaviour is UNDOCUMENTED, so we own the
22
+ // deadline ourselves with a single watchdog + latch (never rely on the
23
+ // host to kill us gracefully).
24
+ // - STDOUT is a single JSON object. Any stray console.log() from a
25
+ // transitively-required module (e.g. signals._mergeSignals, hubSearch
26
+ // fetch-cost log, assetStore seeding) would corrupt it — so we redirect
27
+ // console.* to stderr before requiring anything heavy.
28
+
29
+ 'use strict';
30
+
31
+ // --- stdout-poison defense: route all console.* to stderr ------------------
32
+ // Modules we require (hubSearch, signals, assetStore, …) call console.log,
33
+ // which writes to stdout. The hook contract is ONE JSON object on stdout, so
34
+ // we redirect every console method to stderr. stderr on exit 0 is not fed to
35
+ // the model for UserPromptSubmit; on non-2 exit only its first line shows in
36
+ // the transcript as a hook-error notice — acceptable and we exit 0 anyway.
37
+ for (const m of ['log', 'info', 'warn', 'error', 'debug']) {
38
+ try { console[m] = function () { try { process.stderr.write(''); } catch (_) {} }; } catch (_) {}
39
+ }
40
+
41
+ const path = require('path');
42
+
43
+ // ---- Timing budget, coherent with the host kill ---------------------------
44
+ // The host (Claude Code) kills this process at the hook's `timeout` (5s in
45
+ // buildClaudeHooks -> 5000ms). Our OWN absolute watchdog MUST fire comfortably
46
+ // BEFORE that, or the host could kill us mid-write and break the fail-open
47
+ // stdout contract. So:
48
+ // ABSOLUTE_DEADLINE_MS (3300) < host 5000ms -> ~1.7s headroom for finish().
49
+ // EVOLVER_RECALL_TIMEOUT_MS is the Hub SEARCH budget only (clamped well under
50
+ // the absolute deadline). The actual budget handed to the search is computed
51
+ // DYNAMICALLY at search-start as (deadline - already-elapsed - safety), so
52
+ // slow stdin/require can never let the search run past the watchdog (which
53
+ // would otherwise spuriously return {} — Bugbot #183 medium findings).
54
+ const T0 = Date.now();
55
+ const ABSOLUTE_DEADLINE_MS = 3300; // watchdog; strictly < host timeout (5000ms)
56
+ const SEARCH_SAFETY_MS = 250; // leave room for finish() after the search
57
+ const MIN_SEARCH_MS = 300; // below this, skip the search (finish {})
58
+
59
+ function getSearchBudgetMs() {
60
+ const n = parseInt(process.env.EVOLVER_RECALL_TIMEOUT_MS, 10);
61
+ // Hard cap at 2800 so even a max-budget search starts and ends before the
62
+ // 3300ms watchdog under any realistic startup cost.
63
+ return Number.isFinite(n) && n >= 500 && n <= 2800 ? n : 2000;
64
+ }
65
+
66
+ function getMode() {
67
+ const v = String(process.env.EVOLVER_RECALL_MODE || '').toLowerCase().trim();
68
+ return v === 'shadow' || v === 'enforce' ? v : 'off';
69
+ }
70
+
71
+ let handled = false;
72
+ let watchdog = null;
73
+
74
+ // Single-writer latch: exactly one stdout write, exactly one exit. Mirrors the
75
+ // proven pattern in evolver-signal-detect.js / evolver-session-end.js.
76
+ function finish(obj) {
77
+ if (handled) return;
78
+ handled = true;
79
+ if (watchdog) { try { clearTimeout(watchdog); } catch (_) {} }
80
+ try { process.stdout.write(JSON.stringify(obj || {})); } catch (_) {}
81
+ process.exit(0);
82
+ }
83
+
84
+ function main() {
85
+ const mode = getMode();
86
+
87
+ // Absolute watchdog, armed at process entry and INDEPENDENT of the search
88
+ // budget. Fires at ABSOLUTE_DEADLINE_MS (3300ms) — strictly under the host's
89
+ // timeout (5000ms) so the host can never kill us mid-write. If stdin never
90
+ // closes OR anything hangs, we emit {} and exit cleanly first.
91
+ watchdog = setTimeout(() => finish({}), ABSOLUTE_DEADLINE_MS);
92
+
93
+ let buf = '';
94
+ try {
95
+ process.stdin.setEncoding('utf8');
96
+ } catch (_) { /* some hosts pass no stdin */ }
97
+ process.stdin.on('data', (c) => { buf += c; });
98
+ process.stdin.on('error', () => finish({}));
99
+ process.stdin.on('end', () => {
100
+ if (handled) return;
101
+
102
+ // off: do NOT even parse the prompt body (privacy: nothing read/sent).
103
+ if (mode === 'off') return finish({});
104
+
105
+ let prompt = '';
106
+ let sessionId = '';
107
+ try {
108
+ const input = buf.trim() ? JSON.parse(buf) : {};
109
+ prompt = String(input.prompt || '').trim();
110
+ sessionId = String(input.session_id || input.sessionId || '').trim();
111
+ } catch (_) {
112
+ return finish({});
113
+ }
114
+ if (prompt.length < 8) return finish({}); // trivial prompt -> skip
115
+
116
+ // Heavy require INSIDE try/catch: a broken require graph must fail open,
117
+ // not crash the user's prompt (this is also why the e2e test runs the
118
+ // copied hook with mode=off and asserts exit 0 + parseable stdout).
119
+ let core;
120
+ try {
121
+ const { findEvolverRoot } = require('./_runtimePaths');
122
+ const root = findEvolverRoot();
123
+ if (!root) return finish({});
124
+ core = require(path.join(root, 'src', 'gep', 'recallInject.js'));
125
+ } catch (_) {
126
+ return finish({});
127
+ }
128
+
129
+ // Pass the ABSOLUTE deadline (T0 + watchdog window) plus the configured
130
+ // search cap. The core bounds the Hub call by the time REMAINING to that
131
+ // deadline (minus its own post-await safety margin) and runs local-gene
132
+ // disk I/O BEFORE the Hub await — so neither the Hub search nor the
133
+ // post-processing can overrun the watchdog (Bugbot #183: slow startup or a
134
+ // budget-eating Hub call must not let the timer fire mid-work). If too
135
+ // little time remains even before starting, skip and fail open.
136
+ const elapsed = Date.now() - T0;
137
+ const remaining = ABSOLUTE_DEADLINE_MS - elapsed - SEARCH_SAFETY_MS;
138
+ if (remaining < MIN_SEARCH_MS) return finish({});
139
+ const deadlineMs = T0 + ABSOLUTE_DEADLINE_MS;
140
+
141
+ Promise.resolve()
142
+ .then(() => core.recallForTask({ prompt, mode, sessionId, timeoutMs: getSearchBudgetMs(), deadlineMs }))
143
+ .then((r) => {
144
+ if (r && r.inject && r.text) {
145
+ // Emit BOTH shapes:
146
+ // - nested hookSpecificOutput.additionalContext is the DOCUMENTED
147
+ // canonical UserPromptSubmit injection shape (system-reminder,
148
+ // no transcript noise).
149
+ // - flat additionalContext / agent_message match the in-repo
150
+ // precedent (session-start.js) for hosts that read the flat key.
151
+ // Extra keys are tolerated/ignored by hosts; whichever wins, the
152
+ // other is harmless.
153
+ return finish({
154
+ agent_message: r.text,
155
+ additionalContext: r.text,
156
+ hookSpecificOutput: {
157
+ hookEventName: 'UserPromptSubmit',
158
+ additionalContext: r.text,
159
+ },
160
+ });
161
+ }
162
+ // shadow (logged inside the core) and no-match both inject nothing.
163
+ return finish({});
164
+ })
165
+ .catch(() => finish({}));
166
+ });
167
+ }
168
+
169
+ if (require.main === module) {
170
+ main();
171
+ } else {
172
+ module.exports = { getMode, getSearchBudgetMs };
173
+ }
@@ -59,7 +59,7 @@ function _buildGene(capabilities, signals) {
59
59
  : ['atp_task'];
60
60
  const gene = {
61
61
  type: 'Gene',
62
- schema_version: '1.0',
62
+ schema_version: '1.0.0',
63
63
  id: 'gene_atp_answer_' + caps.sort().join('_').slice(0, 40),
64
64
  summary: 'Deliver an ATP task answer for capabilities: ' + caps.join(', '),
65
65
  signals_match: sig,
@@ -69,6 +69,9 @@ function _buildGene(capabilities, signals) {
69
69
  'Produce a concrete, actionable answer addressing the question directly.',
70
70
  'Return the answer as Capsule content for verifiable delivery.',
71
71
  ],
72
+ // gep-sdk Gene schema requires `constraints`; an ATP answer edits no
73
+ // files, so the blast radius is empty rather than left unbounded.
74
+ constraints: { max_files: 0, forbidden_paths: [] },
72
75
  validation: [
73
76
  'Answer is non-empty and directly addresses the buyer question.',
74
77
  'Answer references the requested capabilities where relevant.',
@@ -86,7 +89,7 @@ function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, s
86
89
  || 'ATP merchant delivery for order ' + String(orderId || '').slice(0, 24);
87
90
  const capsule = {
88
91
  type: 'Capsule',
89
- schema_version: '1.0',
92
+ schema_version: '1.0.0',
90
93
  id: 'capsule_atp_' + String(orderId || taskId || Date.now()).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 40),
91
94
  trigger: sig,
92
95
  gene: gene.id,
@@ -96,11 +99,21 @@ function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, s
96
99
  outcome: { status: 'success', score: confidence },
97
100
  env_fingerprint: { platform: process.platform, arch: process.arch, runtime: 'evolver-atp' },
98
101
  content: answer,
99
- source_type: 'atp_task_executor',
100
- atp: {
101
- order_id: orderId || null,
102
- task_id: taskId || null,
103
- capabilities: caps,
102
+ // 'generated' is the gep-sdk source_type enum value for a freshly
103
+ // produced asset; the ATP-specific provenance rides in `a2a.atp` below.
104
+ source_type: 'generated',
105
+ // The order/task association MUST live under `a2a`, not as a top-level
106
+ // `atp` key: the Hub's payload sanitizer allow-lists `a2a` but not `atp`
107
+ // (CAPSULE_ALLOWED_FIELDS), so a top-level `atp` was being silently
108
+ // stripped on publish and the association never reached the Hub. `a2a`
109
+ // is also an open object in gep-sdk's Capsule schema, so this keeps the
110
+ // bundle GEP-valid.
111
+ a2a: {
112
+ atp: {
113
+ order_id: orderId || null,
114
+ task_id: taskId || null,
115
+ capabilities: caps,
116
+ },
104
117
  },
105
118
  };
106
119
  capsule.asset_id = computeAssetId(capsule);
package/src/atp/cli.js CHANGED
@@ -14,6 +14,14 @@
14
14
  // injectable for tests. Each runner returns a Promise that resolves to
15
15
  // { exitCode: number, output?: string, data?: object }.
16
16
 
17
+ const {
18
+ ATP_VERIFY_MODES,
19
+ ATP_VERIFY_ACTIONS,
20
+ ATP_ROUTING_MODES,
21
+ ATP_PROOF_STATUSES,
22
+ ATP_ROLES,
23
+ } = require('./protocol');
24
+
17
25
  function _parseNamed(args, longFlag, shortFlag) {
18
26
  const long = args.findIndex(a => typeof a === 'string' && (a === longFlag || a.startsWith(longFlag + '=')));
19
27
  if (long !== -1) {
@@ -83,11 +91,11 @@ function parseBuyArgs(args) {
83
91
 
84
92
  function parseOrdersArgs(args) {
85
93
  const role = _parseNamed(args, '--role', null);
86
- if (role && !['consumer', 'merchant'].includes(role)) {
87
- return { ok: false, error: 'invalid --role: ' + role + ' (expected consumer|merchant)' };
94
+ if (role && !ATP_ROLES.includes(role)) {
95
+ return { ok: false, error: 'invalid --role: ' + role + ' (expected ' + ATP_ROLES.join('|') + ')' };
88
96
  }
89
97
  const status = _parseNamed(args, '--status', null);
90
- if (status && !['pending', 'verified', 'disputed', 'settled'].includes(status)) {
98
+ if (status && !ATP_PROOF_STATUSES.includes(status)) {
91
99
  return { ok: false, error: 'invalid --status: ' + status };
92
100
  }
93
101
  const limitRaw = _parseNamed(args, '--limit', null);
@@ -112,8 +120,8 @@ function parseVerifyArgs(args) {
112
120
  return { ok: false, error: 'verify requires <orderId>' };
113
121
  }
114
122
  const action = _parseNamed(args, '--action', null) || 'confirm';
115
- if (!['confirm', 'ai_judge'].includes(action)) {
116
- return { ok: false, error: 'invalid --action: ' + action + ' (expected confirm|ai_judge)' };
123
+ if (!ATP_VERIFY_ACTIONS.includes(action)) {
124
+ return { ok: false, error: 'invalid --action: ' + action + ' (expected ' + ATP_VERIFY_ACTIONS.join('|') + ')' };
117
125
  }
118
126
  return { ok: true, opts: { orderId, action } };
119
127
  }
@@ -324,11 +332,11 @@ async function runAtp(opts, deps) {
324
332
  function printUsage() {
325
333
  return [
326
334
  'ATP subcommands:',
327
- ' evolver buy <caps> [--budget=N] [--question "..."] [--routing=fastest|cheapest|auction|swarm]',
328
- ' [--verify=auto|ai_judge|bilateral] [--no-wait] [--timeout=<seconds>]',
329
- ' evolver orders [--role=consumer|merchant] [--status=pending|verified|disputed|settled]',
335
+ ' evolver buy <caps> [--budget=N] [--question "..."] [--routing=' + ATP_ROUTING_MODES.join('|') + ']',
336
+ ' [--verify=' + ATP_VERIFY_MODES.join('|') + '] [--no-wait] [--timeout=<seconds>]',
337
+ ' evolver orders [--role=' + ATP_ROLES.join('|') + '] [--status=' + ATP_PROOF_STATUSES.join('|') + ']',
330
338
  ' [--limit=N] [--json]',
331
- ' evolver verify <orderId> [--action=confirm|ai_judge]',
339
+ ' evolver verify <orderId> [--action=' + ATP_VERIFY_ACTIONS.join('|') + ']',
332
340
  ' evolver atp <enable|disable|status> -- manage auto-spend consent',
333
341
  ].join('\n');
334
342
  }
@@ -0,0 +1,41 @@
1
+ // Protocol-level enum constants for the Agent Transaction Protocol.
2
+ //
3
+ // These values (verify modes, routing modes, proof statuses, roles,
4
+ // execution modes) live in @evomap/atp-sdk. This module is a thin
5
+ // CommonJS facade so callsites in src/atp/ can `require('./protocol')`
6
+ // for the authoritative sets instead of re-spelling the literals.
7
+ //
8
+ // Why move them out: ATP is the wire contract between this engine, the
9
+ // EvoMap Hub, and (in future) evox-Rust. Hand-maintaining the allowed
10
+ // value sets in each implementation is exactly the drift that the
11
+ // v1.80.8 "explore" enum incident taught us to avoid for GEP. The ATP
12
+ // contract is extracted into its own SDK before a second runtime wires
13
+ // in, while it is still cheap. If you find yourself writing an enum
14
+ // list literal here again (e.g. ['pending','verified',...]), stop --
15
+ // import the constant from this facade instead, and bump
16
+ // @evomap/atp-sdk if the set itself needs to change.
17
+ //
18
+ // Implementation note: @evomap/atp-sdk is published as ESM
19
+ // (`"type": "module"`). Node supports `require()` of synchronous ESM
20
+ // packages on 22.12.0 and later. The SDK itself stays permissive
21
+ // (`engines.node >=18`) so `import`-based consumers on 18/20 aren't
22
+ // blocked; the `>=22.12` guarantee that makes the require() below work
23
+ // is pinned in THIS package's (evolver's) `engines.node`, not the SDK's.
24
+
25
+ const {
26
+ ATP_VERIFY_MODES,
27
+ ATP_VERIFY_ACTIONS,
28
+ ATP_ROUTING_MODES,
29
+ ATP_PROOF_STATUSES,
30
+ ATP_ROLES,
31
+ ATP_EXECUTION_MODES,
32
+ } = require('@evomap/atp-sdk');
33
+
34
+ module.exports = {
35
+ ATP_VERIFY_MODES,
36
+ ATP_VERIFY_ACTIONS,
37
+ ATP_ROUTING_MODES,
38
+ ATP_PROOF_STATUSES,
39
+ ATP_ROLES,
40
+ ATP_EXECUTION_MODES,
41
+ };
package/src/config.js CHANGED
@@ -136,9 +136,14 @@ const REPAIR_LOOP_THRESHOLD = envInt('EVOLVER_REPAIR_LOOP_THRESHOLD', 3);
136
136
  //
137
137
  // GENE_BAN_PER_KEY_ATTEMPTS: minimum attempts on the same signal key
138
138
  // GENE_BAN_BEST_THRESHOLD: best success rate at or below which the Gene is banned
139
+ // GENE_INERT_BAN_STREAK: consecutive inert (stable_no_error, zero-work) outcomes
140
+ // on a signal key after which a Gene with no real
141
+ // success is banned, so --loop selection explores
142
+ // instead of re-running a do-nothing gene (#562)
139
143
  // GENE_EPIGENETIC_HARD_BOOST: epigenetic boost at or below which the Gene is hard-suppressed
140
144
  const GENE_BAN_PER_KEY_ATTEMPTS = envInt('EVOLVER_GENE_BAN_PER_KEY_ATTEMPTS', 4);
141
145
  const GENE_BAN_BEST_THRESHOLD = envFloat('EVOLVER_GENE_BAN_BEST_THRESHOLD', 0.15);
146
+ const GENE_INERT_BAN_STREAK = envInt('EVOLVER_GENE_INERT_BAN_STREAK', 8);
142
147
  const GENE_EPIGENETIC_HARD_BOOST = envFloat('EVOLVER_GENE_EPIGENETIC_HARD_BOOST', -0.3);
143
148
  const SESSION_ARCHIVE_TRIGGER = envInt('EVOLVER_SESSION_ARCHIVE_TRIGGER', 100);
144
149
  const SESSION_ARCHIVE_KEEP = envInt('EVOLVER_SESSION_ARCHIVE_KEEP', 50);
@@ -177,6 +182,26 @@ const SELF_PR_TIMEOUT_MS = envInt('EVOLVER_SELF_PR_TIMEOUT_MS', 30000);
177
182
 
178
183
  const LEAK_CHECK_MODE = envStr('EVOLVER_LEAK_CHECK', 'strict');
179
184
 
185
+ // --- Reuse attribution (P4-a, Slice A) ---
186
+ // Controls whether the evolver attaches a `reuse_attribution` block to the
187
+ // synced `outcome` MemoryGraphEvent so the Hub can LATER (P4-a Slice B, gated +
188
+ // team-signed-off) credit the SOURCE node when its asset is reused. Modes:
189
+ // off (default) — attach nothing; byte-identical to pre-P4-a behavior.
190
+ // shadow — attach the attribution block; it rides the existing
191
+ // syncEventToHub -> /a2a/memory/event into the Hub's
192
+ // MemoryGraphEvent.payload blob, which is GDI-inert and
193
+ // read by NO payout path today.
194
+ // There is intentionally NO `enforce` on the CLIENT: the evolver cannot move
195
+ // money, so an enforce word would be a lie. The report stays economy-inert
196
+ // until a SIGNED-OFF Hub reader converts it to credit (which MUST add the
197
+ // anti-sybil gating — see P4-a Slice B). Until then it only emits honest,
198
+ // runtime-observed attribution data (never agent-supplied identity).
199
+ const REUSE_ATTRIBUTION_MODE = envStr('EVOLVER_REUSE_ATTRIBUTION', 'off');
200
+ function reuseAttributionMode() {
201
+ const v = String(process.env.EVOLVER_REUSE_ATTRIBUTION || REUSE_ATTRIBUTION_MODE || 'off').toLowerCase().trim();
202
+ return v === 'shadow' ? 'shadow' : 'off';
203
+ }
204
+
180
205
  // --- Validator mode (opt-out) ---
181
206
  // Node role: the evolver periodically fetches assigned validation tasks from
182
207
  // the Hub, runs the commands in an isolated sandbox, and submits
@@ -223,6 +248,7 @@ module.exports = {
223
248
  REPAIR_LOOP_THRESHOLD,
224
249
  GENE_BAN_PER_KEY_ATTEMPTS,
225
250
  GENE_BAN_BEST_THRESHOLD,
251
+ GENE_INERT_BAN_STREAK,
226
252
  GENE_EPIGENETIC_HARD_BOOST,
227
253
  SESSION_ARCHIVE_TRIGGER,
228
254
  SESSION_ARCHIVE_KEEP,
@@ -257,6 +283,9 @@ module.exports = {
257
283
  BLAST_RADIUS_HARD_CAP_LINES,
258
284
  // Security
259
285
  LEAK_CHECK_MODE,
286
+ // Reuse attribution (P4-a Slice A)
287
+ REUSE_ATTRIBUTION_MODE,
288
+ reuseAttributionMode,
260
289
  // Validator (opt-in role)
261
290
  VALIDATOR_ENABLED,
262
291
  VALIDATOR_STAKE_AMOUNT,