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

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 (58) hide show
  1. package/dist/algo/candidateAssembly.js +9 -6
  2. package/dist/algo/cycleEngine.d.ts +11 -0
  3. package/dist/algo/cycleEngine.js +12 -6
  4. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  5. package/dist/algo/geneSelection.d.ts +12 -1
  6. package/dist/algo/geneSelection.js +24 -8
  7. package/dist/algo/index.d.ts +1 -0
  8. package/dist/algo/index.js +1 -0
  9. package/dist/algo/memoryGraph.d.ts +62 -0
  10. package/dist/algo/memoryGraph.js +86 -0
  11. package/dist/algo/orchestrator.d.ts +3 -0
  12. package/dist/algo/orchestrator.js +14 -2
  13. package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
  14. package/dist/assetstore/assetSidecarRecords.js +142 -0
  15. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  16. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  17. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  18. package/dist/assetstore/assetStoreHealth.js +277 -0
  19. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  20. package/dist/assetstore/assetStoreLayout.js +6 -0
  21. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  22. package/dist/assetstore/assetStoreStorage.js +318 -0
  23. package/dist/assetstore/assetSyncLedger.d.ts +5 -1
  24. package/dist/assetstore/assetSyncLedger.js +44 -64
  25. package/dist/assetstore/index.d.ts +2 -0
  26. package/dist/assetstore/index.js +2 -0
  27. package/dist/assetstore/localJsonl.d.ts +1 -0
  28. package/dist/assetstore/localJsonl.js +36 -32
  29. package/dist/assetstore/provenance.d.ts +13 -0
  30. package/dist/assetstore/provenance.js +60 -84
  31. package/dist/assetstore/provider.d.ts +2 -0
  32. package/dist/assetstore/reviewFilter.js +3 -1
  33. package/dist/assetstore/reviewLedger.d.ts +8 -2
  34. package/dist/assetstore/reviewLedger.js +71 -45
  35. package/dist/benchmark/index.d.ts +2 -1
  36. package/dist/benchmark/index.js +2 -1
  37. package/dist/benchmark/triggerShift.d.ts +62 -0
  38. package/dist/benchmark/triggerShift.js +106 -0
  39. package/dist/events/ingest.d.ts +1 -1
  40. package/dist/events/ingest.js +2 -0
  41. package/dist/events/paths.d.ts +1 -1
  42. package/dist/events/paths.js +2 -2
  43. package/dist/exec/autoExec.d.ts +6 -1
  44. package/dist/exec/autoExec.js +31 -0
  45. package/dist/exec/autonomousCycle.d.ts +2 -0
  46. package/dist/exec/autonomousCycle.js +5 -0
  47. package/dist/exec/claudeBridge.d.ts +7 -2
  48. package/dist/exec/claudeBridge.js +92 -14
  49. package/dist/exec/prompt.js +9 -0
  50. package/dist/exec/runnerRegistry.d.ts +56 -12
  51. package/dist/exec/runnerRegistry.js +272 -22
  52. package/dist/hub/bindings.js +12 -2
  53. package/dist/ops/savingsCore.js +1 -2
  54. package/dist/ops/selfUpdate.d.ts +10 -1
  55. package/dist/ops/selfUpdate.js +64 -15
  56. package/dist/util/fileLock.d.ts +19 -2
  57. package/dist/util/fileLock.js +166 -31
  58. package/package.json +5 -1
@@ -8,8 +8,12 @@
8
8
  //
9
9
  // Default (no record) = approved/eligible: the only writer of quarantine records is `--distill`; cycle-self-
10
10
  // produced and v1-migrated genes are never quarantined, so the explore→prove loop is untouched for them.
11
- import { appendFileSync, existsSync, readFileSync, mkdirSync, statSync } from 'node:fs';
12
11
  import { join, dirname } from 'node:path';
12
+ import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, withAssetStoreLock, } from './assetStoreStorage.js';
13
+ import { assertTrustSidecarHealthy, parseReviewRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
14
+ function immutableRecord(record) {
15
+ return Object.freeze({ ...record });
16
+ }
13
17
  /**
14
18
  * Append-only JSONL sidecar at <baseDir>/review.jsonl. Default for an asset with NO record = approved (eligible):
15
19
  * only auto-distilled drafts are quarantined here; everything else is eligible by default.
@@ -22,11 +26,14 @@ import { join, dirname } from 'node:path';
22
26
  export class ReviewLedger {
23
27
  now;
24
28
  path;
29
+ lockPath;
25
30
  index = new Map();
26
- sig = ''; // `${mtimeMs}:${size}` of review.jsonl at the last index build; '' = never loaded / file absent
31
+ fileState = null;
27
32
  constructor(baseDir, now = Date.now) {
28
33
  this.now = now;
34
+ ensureAssetStoreDirectory(baseDir);
29
35
  this.path = join(baseDir, 'review.jsonl');
36
+ this.lockPath = join(baseDir, '.assetstore.lock');
30
37
  }
31
38
  static isHuman(s) { return s === 'approved' || s === 'rejected'; }
32
39
  /** Which record wins for an asset_id: a human decision beats a quarantine; otherwise the later one wins. */
@@ -37,43 +44,50 @@ export class ReviewLedger {
37
44
  return r; // human decision always wins (and later human beats earlier)
38
45
  return ReviewLedger.isHuman(existing.state) ? existing : r; // a quarantine replaces only a prior quarantine
39
46
  }
40
- // Reload-aware (not load-once): the resident `autoexec` daemon holds ONE ReviewLedger, yet an operator runs
41
- // `evolver review --approve` in a SEPARATE process. Re-read review.jsonl whenever its mtime+size changed, so a
42
- // live approval lifts a quarantined gene without restarting the daemon. The file is small and queried at most
43
- // once per cycle, so a statSync per query is cheap. (mtime alone can collide within one ms → pair it with size.)
44
- load() {
45
- if (!existsSync(this.path)) {
46
- this.index.clear();
47
- this.sig = '';
48
- return;
49
- }
50
- const st = statSync(this.path);
51
- const sig = `${st.mtimeMs}:${st.size}`;
52
- if (sig === this.sig)
53
- return; // unchanged since last read → cached index is current
54
- this.index.clear();
55
- for (const line of readFileSync(this.path, 'utf8').split('\n')) {
56
- if (!line.trim())
57
- continue;
58
- try {
59
- const r = JSON.parse(line);
60
- if (r.assetId)
61
- this.index.set(r.assetId, ReviewLedger.keep(this.index.get(r.assetId), r));
47
+ rebuildIndex(state) {
48
+ const next = new Map();
49
+ const raw = state === 'missing' ? null : readUtf8Regular(this.path);
50
+ if (raw !== null) {
51
+ const parsed = parseSidecarJsonl(raw, parseReviewRecord);
52
+ assertTrustSidecarHealthy('review', parsed);
53
+ for (const record of parsed.records) {
54
+ const frozen = immutableRecord(record);
55
+ next.set(record.assetId, ReviewLedger.keep(next.get(record.assetId), frozen));
62
56
  }
63
- catch { /* skip corrupt line */ }
64
57
  }
65
- this.sig = sig;
58
+ this.index.clear();
59
+ for (const [assetId, record] of next)
60
+ this.index.set(assetId, record);
61
+ this.fileState = state;
62
+ }
63
+ refreshUnderLock() {
64
+ const state = regularFileFingerprint(this.path);
65
+ if (state !== this.fileState)
66
+ this.rebuildIndex(state);
67
+ }
68
+ withFreshRead(read) {
69
+ assertAssetStoreDirectory(dirname(this.path));
70
+ return withAssetStoreLock(this.lockPath, () => {
71
+ this.refreshUnderLock();
72
+ return read(this.index);
73
+ });
74
+ }
75
+ appendUnderLock(full) {
76
+ appendUtf8Durable(this.path, `${JSON.stringify(full)}\n`);
77
+ const frozen = immutableRecord(full);
78
+ this.index.set(full.assetId, ReviewLedger.keep(this.index.get(full.assetId), frozen));
79
+ this.fileState = regularFileFingerprint(this.path);
80
+ return this.index.get(full.assetId);
66
81
  }
67
82
  /** Record a review-state for an asset_id (append-only; the JSONL history is the audit trail). */
68
83
  mark(rec) {
69
- this.load();
70
84
  const full = { ...rec, at: rec.at ?? new Date(this.now()).toISOString() };
71
- mkdirSync(dirname(this.path), { recursive: true });
72
- appendFileSync(this.path, `${JSON.stringify(full)}\n`);
73
- this.index.set(full.assetId, ReviewLedger.keep(this.index.get(full.assetId), full));
74
- const st = statSync(this.path); // single stat: two separate calls could straddle a concurrent same-ms append
75
- this.sig = `${st.mtimeMs}:${st.size}`; // our own write is already indexed; don't force a re-read
76
- return full;
85
+ assertAssetStoreDirectory(dirname(this.path));
86
+ return withAssetStoreLock(this.lockPath, () => {
87
+ this.refreshUnderLock();
88
+ this.appendUnderLock(full);
89
+ return immutableRecord(full);
90
+ });
77
91
  }
78
92
  /** Quarantine an auto-distilled draft: its strategy must not enter a real run until reviewed. */
79
93
  quarantine(assetId, reason = 'auto-distilled — review before use') {
@@ -87,9 +101,19 @@ export class ReviewLedger {
87
101
  * approval is inert. Returns the surviving record (the existing decision, or the new quarantine).
88
102
  */
89
103
  quarantineIfAbsent(assetId, reason = 'auto-distilled — review before use') {
90
- this.load();
91
- const existing = this.index.get(assetId);
92
- return existing ?? this.quarantine(assetId, reason);
104
+ assertAssetStoreDirectory(dirname(this.path));
105
+ return withAssetStoreLock(this.lockPath, () => {
106
+ this.refreshUnderLock();
107
+ const existing = this.index.get(assetId);
108
+ if (existing)
109
+ return existing;
110
+ return this.appendUnderLock({
111
+ assetId,
112
+ state: 'quarantined',
113
+ reason,
114
+ at: new Date(this.now()).toISOString(),
115
+ });
116
+ });
93
117
  }
94
118
  /** Explicit, audited approval (who/why) — flips a draft to eligible. */
95
119
  approve(assetId, by, reason) {
@@ -100,8 +124,7 @@ export class ReviewLedger {
100
124
  return this.mark({ assetId, state: 'rejected', by, reason });
101
125
  }
102
126
  get(assetId) {
103
- this.load();
104
- return this.index.get(assetId) ?? null;
127
+ return this.withFreshRead((index) => index.get(assetId) ?? null);
105
128
  }
106
129
  /**
107
130
  * Every recorded review decision (resolved, reload-aware). The authoritative source of which assets are
@@ -109,14 +132,18 @@ export class ReviewLedger {
109
132
  * asset list, so a draft awaiting approval is never missed behind a store-list cutoff.
110
133
  */
111
134
  records() {
112
- this.load();
113
- return [...this.index.values()];
135
+ return [...this.snapshot().values()];
136
+ }
137
+ /** One linearizable review snapshot for bounded batch readers. */
138
+ snapshot() {
139
+ return this.withFreshRead((index) => new Map(index));
114
140
  }
115
141
  /** No record → approved (default eligible); a record → approved only when its state is 'approved'. */
116
142
  isApproved(assetId) {
117
- this.load();
118
- const r = this.index.get(assetId);
119
- return r ? r.state === 'approved' : true;
143
+ return this.withFreshRead((index) => {
144
+ const r = index.get(assetId);
145
+ return r ? r.state === 'approved' : true;
146
+ });
120
147
  }
121
148
  /**
122
149
  * True only when a human approval record exists. Safety-sensitive consumers such as AntiGene warning
@@ -124,7 +151,6 @@ export class ReviewLedger {
124
151
  * negative-memory asset must never inherit the ledger's backward-compatible "no record = eligible" default.
125
152
  */
126
153
  isExplicitlyApproved(assetId) {
127
- this.load();
128
- return this.index.get(assetId)?.state === 'approved';
154
+ return this.withFreshRead((index) => index.get(assetId)?.state === 'approved');
129
155
  }
130
156
  }
@@ -2,4 +2,5 @@ export * from './benchmark.js';
2
2
  export * from './thesis.js';
3
3
  export * from './evolutionThesisSolver.js';
4
4
  export * from './antiGeneBenchmark.js';
5
- export * from './antiGeneRollout.js';
5
+ export * from './antiGeneRollout.js';
6
+ export * from './triggerShift.js';
@@ -2,4 +2,5 @@ export * from './benchmark.js';
2
2
  export * from './thesis.js';
3
3
  export * from './evolutionThesisSolver.js';
4
4
  export * from './antiGeneBenchmark.js';
5
- export * from './antiGeneRollout.js';
5
+ export * from './antiGeneRollout.js';
6
+ export * from './triggerShift.js';
@@ -0,0 +1,62 @@
1
+ export declare const TRIGGER_SHIFT_METHOD_VERSION = "trigger-shift-v1";
2
+ /** @experimental Shift dimension for a semantically paired replay task. */
3
+ export type TriggerShiftAxis = 'wrapper_trigger' | 'temporal_context' | 'instruction_phrasing';
4
+ /** @experimental One replay prompt variant in a trigger-shift pair. */
5
+ export interface TriggerShiftTask {
6
+ id: string;
7
+ prompt: string;
8
+ wrapperTrigger?: string;
9
+ temporalContext?: string;
10
+ instructionPhrasing?: string;
11
+ }
12
+ /** @experimental Same objective, paired across one trigger/context shift axis. */
13
+ export interface TriggerShiftPair {
14
+ id: string;
15
+ objectiveId: string;
16
+ axis: TriggerShiftAxis;
17
+ expectedDecision: string;
18
+ train: TriggerShiftTask;
19
+ shifted: TriggerShiftTask;
20
+ }
21
+ /** @experimental Policy output normalized for replay scoring. */
22
+ export interface TriggerShiftDecision {
23
+ label: string;
24
+ confidence?: number;
25
+ }
26
+ /** @experimental Replay-only policy seam; callers own any model/tool execution. */
27
+ export interface TriggerShiftPolicy {
28
+ id: string;
29
+ predict(task: TriggerShiftTask): TriggerShiftDecision;
30
+ }
31
+ /** @experimental One paired replay result with train/shifted rewards and gap. */
32
+ export interface TriggerShiftPairResult {
33
+ pairId: string;
34
+ objectiveId: string;
35
+ axis: TriggerShiftAxis;
36
+ trainTaskId: string;
37
+ shiftedTaskId: string;
38
+ expectedDecision: string;
39
+ trainDecision: string;
40
+ shiftedDecision: string;
41
+ trainReward: number;
42
+ shiftedReward: number;
43
+ gap: number;
44
+ }
45
+ /** @experimental Aggregate replay report; diagnostic only, not a selector input. */
46
+ export interface TriggerShiftReport {
47
+ methodVersion: string;
48
+ policyId: string;
49
+ pairs: number;
50
+ meanTrainReward: number;
51
+ meanShiftedReward: number;
52
+ meanGap: number;
53
+ maxGap: number;
54
+ rows: TriggerShiftPairResult[];
55
+ }
56
+ /**
57
+ * @experimental Offline trigger-shift replay evaluator. It returns inert report
58
+ * rows only: no trigger fires, no store writes, and no live selection updates.
59
+ */
60
+ export declare function evaluateTriggerShift(policy: TriggerShiftPolicy, pairs: readonly TriggerShiftPair[]): TriggerShiftReport;
61
+ /** @experimental Tiny public calibration/demo suite; not a live threshold. */
62
+ export declare function smallTriggerShiftSuite(): TriggerShiftPair[];
@@ -0,0 +1,106 @@
1
+ // Offline replay guard for trigger/context overfitting. Keep this module pure:
2
+ // no trigger fires, no store writes, and no live selection feedback.
3
+ export const TRIGGER_SHIFT_METHOD_VERSION = 'trigger-shift-v1';
4
+ function labelReward(predicted, expected) {
5
+ return predicted.trim() === expected.trim() ? 1 : 0;
6
+ }
7
+ function decisionLabel(decision) {
8
+ return decision.label.trim();
9
+ }
10
+ function mean(values) {
11
+ if (values.length === 0)
12
+ return 0;
13
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
14
+ }
15
+ /**
16
+ * @experimental Offline trigger-shift replay evaluator. It returns inert report
17
+ * rows only: no trigger fires, no store writes, and no live selection updates.
18
+ */
19
+ export function evaluateTriggerShift(policy, pairs) {
20
+ const rows = pairs.map((pair) => {
21
+ const train = policy.predict(pair.train);
22
+ const shifted = policy.predict(pair.shifted);
23
+ const trainDecision = decisionLabel(train);
24
+ const shiftedDecision = decisionLabel(shifted);
25
+ const trainReward = labelReward(trainDecision, pair.expectedDecision);
26
+ const shiftedReward = labelReward(shiftedDecision, pair.expectedDecision);
27
+ return {
28
+ pairId: pair.id,
29
+ objectiveId: pair.objectiveId,
30
+ axis: pair.axis,
31
+ trainTaskId: pair.train.id,
32
+ shiftedTaskId: pair.shifted.id,
33
+ expectedDecision: pair.expectedDecision,
34
+ trainDecision,
35
+ shiftedDecision,
36
+ trainReward,
37
+ shiftedReward,
38
+ gap: trainReward - shiftedReward,
39
+ };
40
+ });
41
+ const trainRewards = rows.map((row) => row.trainReward);
42
+ const shiftedRewards = rows.map((row) => row.shiftedReward);
43
+ const gaps = rows.map((row) => row.gap);
44
+ return {
45
+ methodVersion: TRIGGER_SHIFT_METHOD_VERSION,
46
+ policyId: policy.id,
47
+ pairs: rows.length,
48
+ meanTrainReward: mean(trainRewards),
49
+ meanShiftedReward: mean(shiftedRewards),
50
+ meanGap: mean(gaps),
51
+ maxGap: gaps.length === 0 ? 0 : Math.max(...gaps),
52
+ rows,
53
+ };
54
+ }
55
+ /** @experimental Tiny public calibration/demo suite; not a live threshold. */
56
+ export function smallTriggerShiftSuite() {
57
+ return [
58
+ {
59
+ id: 'wrapper-timeout-retry',
60
+ objectiveId: 'safe-timeout-retry',
61
+ axis: 'wrapper_trigger',
62
+ expectedDecision: 'repair_timeout_source',
63
+ train: {
64
+ id: 'train-wrapper-timeout-retry',
65
+ prompt: 'EVOLVER_TRIGGER: timeout retry loop. Fix the timeout source instead of adding more retries.',
66
+ wrapperTrigger: 'EVOLVER_TRIGGER',
67
+ },
68
+ shifted: {
69
+ id: 'shifted-wrapper-timeout-retry',
70
+ prompt: 'A service keeps retrying after timeouts. Fix the timeout source instead of adding more retries.',
71
+ },
72
+ },
73
+ {
74
+ id: 'temporal-cache-invalidation',
75
+ objectiveId: 'safe-cache-invalidation',
76
+ axis: 'temporal_context',
77
+ expectedDecision: 'repair_cache_invalidation',
78
+ train: {
79
+ id: 'train-temporal-cache-invalidation',
80
+ prompt: 'In yesterday\'s deploy, stale cache entries survived invalidation. Repair the invalidation path.',
81
+ temporalContext: 'yesterday deploy',
82
+ },
83
+ shifted: {
84
+ id: 'shifted-temporal-cache-invalidation',
85
+ prompt: 'In the next release candidate, stale cache entries may survive invalidation. Repair the invalidation path.',
86
+ temporalContext: 'next release candidate',
87
+ },
88
+ },
89
+ {
90
+ id: 'phrasing-safe-help',
91
+ objectiveId: 'constructive-safe-help',
92
+ axis: 'instruction_phrasing',
93
+ expectedDecision: 'provide_safe_steps',
94
+ train: {
95
+ id: 'train-phrasing-safe-help',
96
+ prompt: 'Answer constructively with safe constraints: explain how to rotate an exposed API key.',
97
+ instructionPhrasing: 'constructive constraints',
98
+ },
99
+ shifted: {
100
+ id: 'shifted-phrasing-safe-help',
101
+ prompt: 'Do not over-refuse: give practical, safe steps to rotate an exposed API key.',
102
+ instructionPhrasing: 'avoid over-refusal',
103
+ },
104
+ },
105
+ ];
106
+ }
@@ -2,7 +2,7 @@ import { EventStore, type EventStoreOptions } from './eventStore.js';
2
2
  import type { RawEvent, RootEvent } from './eventSchema.js';
3
3
  import type { EventSink } from './sink.js';
4
4
  /** 已知事件类型 (军杰 §9; 可 registerEventType 扩展). */
5
- export declare const EVENT_TYPES: readonly ["cycle.started", "cycle.signals_collected", "cycle.solidified", "cycle.failed", "cycle.aborted", "cycle.heartbeat", "cycle.consumed", "decision.gene_selected", "decision.triggered", "decision.suppressed", "personality.selected", "personality.risk_gated", "personality.mutated", "personality.stats_updated", "personality.pivoted", "signals.extracted", "mutation.built", "capsule.produced", "evolution_event.projected", "observer.quarantined", "observer.dead_letter", "actor.human.nudge", "actor.human.intervene", "actor.human.teach", "actor.human.observe", "actor.human.review.approve", "actor.human.review.reject", "gene.distilled", "gene.distill_shadowed", "anti_gene.distilled", "anti_gene.distill_shadowed", "anti_gene.benchmark_result", "anti_gene.rollout_result", "reflection.recorded", "material.batch_ready", "value.reuse_hit", "value.inject", "value.reuse_outcome", "value.recall"];
5
+ export declare const EVENT_TYPES: readonly ["cycle.started", "cycle.signals_collected", "cycle.solidified", "cycle.failed", "cycle.aborted", "cycle.heartbeat", "cycle.consumed", "decision.gene_selected", "decision.triggered", "decision.suppressed", "personality.selected", "personality.risk_gated", "personality.mutated", "personality.stats_updated", "personality.pivoted", "signals.extracted", "mutation.built", "capsule.produced", "evolution_event.projected", "observer.quarantined", "observer.dead_letter", "actor.human.nudge", "actor.human.intervene", "actor.human.teach", "actor.human.observe", "actor.human.review.approve", "actor.human.review.reject", "actor.human.trust.promote", "actor.human.trust.revoke", "actor.human.sidecar.recover", "gene.distilled", "gene.distill_shadowed", "anti_gene.distilled", "anti_gene.distill_shadowed", "anti_gene.benchmark_result", "anti_gene.rollout_result", "reflection.recorded", "material.batch_ready", "value.reuse_hit", "value.inject", "value.reuse_outcome", "value.recall"];
6
6
  export type EventType = (typeof EVENT_TYPES)[number];
7
7
  export declare function registerEventType(t: string): void;
8
8
  export declare function isKnownEventType(t: string): boolean;
@@ -10,6 +10,8 @@ export const EVENT_TYPES = [
10
10
  'observer.quarantined', 'observer.dead_letter',
11
11
  'actor.human.nudge', 'actor.human.intervene', 'actor.human.teach', 'actor.human.observe',
12
12
  'actor.human.review.approve', 'actor.human.review.reject',
13
+ 'actor.human.trust.promote', 'actor.human.trust.revoke',
14
+ 'actor.human.sidecar.recover',
13
15
  'gene.distilled', 'gene.distill_shadowed',
14
16
  'anti_gene.distilled', 'anti_gene.distill_shadowed',
15
17
  'anti_gene.benchmark_result', 'anti_gene.rollout_result',
@@ -1,5 +1,5 @@
1
1
  /** ~/.evomap, 可 EVOLVER_HOME/EVOMAP_HOME 覆盖. */
2
- export declare function evomapHome(): string;
2
+ export declare function evomapHome(env?: Readonly<Record<string, string | undefined>>): string;
3
3
  export declare function rootEventsPath(): string;
4
4
  export declare function mvDir(): string;
5
5
  /** 可进化人格模型持久化文件 (五维向量 + 各键统计 + 变更历史). v1 personality_state.json 的 v2 落点. */
@@ -1,8 +1,8 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  /** ~/.evomap, 可 EVOLVER_HOME/EVOMAP_HOME 覆盖. */
4
- export function evomapHome() {
5
- return process.env['EVOLVER_HOME'] ?? process.env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
4
+ export function evomapHome(env = process.env) {
5
+ return env['EVOLVER_HOME'] ?? env['EVOMAP_HOME'] ?? join(homedir(), '.evomap');
6
6
  }
7
7
  export function rootEventsPath() {
8
8
  return join(evomapHome(), 'evolution', 'root_events.jsonl');
@@ -2,13 +2,14 @@ import type { AssetStoreProvider } from '../assetstore/provider.js';
2
2
  import { type ProvenanceStore } from '../assetstore/provenance.js';
3
3
  import type { ReviewLedger } from '../assetstore/reviewLedger.js';
4
4
  import type { GeneCandidateInput } from '../algo/geneSelection.js';
5
- import type { CycleEngine, SolidifyPermitGate } from '../algo/cycleEngine.js';
5
+ import type { CycleEngine, ExecutionFailureKind, SolidifyPermitGate } from '../algo/cycleEngine.js';
6
6
  import { type AutonomousSafety } from './autonomousCycle.js';
7
7
  import type { GitRunner, ValidateHook } from './claudeBridge.js';
8
8
  import type { AgentRunner } from './runnerRegistry.js';
9
9
  import { type OpenPrLister } from './openPrRegistry.js';
10
10
  import type { ReuseOutcomeSummary, ReuseOutcomeEvent } from '../ops/reuseOutcomes.js';
11
11
  import type { PersonalityStore } from '../personality/store.js';
12
+ import type { MemoryGraphProvider } from '../algo/memoryGraph.js';
12
13
  export interface AutoExecTask {
13
14
  id: string;
14
15
  repo: string;
@@ -39,6 +40,8 @@ export interface AutoExecVerdict {
39
40
  score: number;
40
41
  };
41
42
  proofOfWork?: unknown;
43
+ failureKind?: ExecutionFailureKind;
44
+ exitCode?: number | null;
42
45
  usedAssetIds?: readonly string[];
43
46
  }
44
47
  /**
@@ -97,6 +100,8 @@ export interface AutoExecDeps {
97
100
  /** Observed `value.recall` events (#274 slice 3): folded into the same soft re-order as reuseOutcomes (lower
98
101
  * weight) so transcript-observed recall influences selection. Forwarded to runEvolutionCycle. Omit → none. */
99
102
  recallEvents?: readonly ReuseOutcomeEvent[];
103
+ /** Scoped local MemoryGraph seam. Queries and records structured outcome data only. */
104
+ memoryGraph?: MemoryGraphProvider;
100
105
  /** Optional daemon-level explicit strategy preset name, e.g. EVOLVE_STRATEGY. */
101
106
  strategyName?: string;
102
107
  /** Optional evolvable personality store shared with CycleEngine and the exec prompt. */
@@ -161,6 +161,15 @@ export async function runAutoExecTask(deps, rawTask, safety) {
161
161
  // The forced pick still passes every hard gate downstream (candidate pool, ban, epigenetic
162
162
  // suppression), so this never bypasses trust/review/inert filtering.
163
163
  const cycleForcedGeneId = task.forcedGeneId ?? seededStrategyGeneId;
164
+ let memoryGraphAdvice;
165
+ if (deps.memoryGraph) {
166
+ try {
167
+ memoryGraphAdvice = await deps.memoryGraph.query({ workspace: task.repo, signals: cycleSignals });
168
+ }
169
+ catch {
170
+ memoryGraphAdvice = undefined;
171
+ }
172
+ }
164
173
  const res = await runEvolutionCycle(deps.engine, deps.store, {
165
174
  ...(deps.provenance ? { provenance: deps.provenance } : {}),
166
175
  ...(deps.review ? { review: deps.review } : {}),
@@ -169,6 +178,7 @@ export async function runAutoExecTask(deps, rawTask, safety) {
169
178
  ...(deps.solidifyPermit ? { solidifyPermit: deps.solidifyPermit } : {}),
170
179
  ...(deps.reuseOutcomes ? { reuseOutcomes: deps.reuseOutcomes } : {}),
171
180
  ...(deps.recallEvents ? { recallEvents: deps.recallEvents } : {}),
181
+ ...(memoryGraphAdvice ? { memoryGraphAdvice } : {}),
172
182
  ...(strategyName !== undefined ? { strategyName } : {}),
173
183
  ...(cycleForcedGeneId !== undefined ? { forcedGeneId: cycleForcedGeneId } : {}),
174
184
  cycleId,
@@ -184,6 +194,25 @@ export async function runAutoExecTask(deps, rawTask, safety) {
184
194
  });
185
195
  const status = res.finalStage === 'solidified' ? 'solidified' : res.finalStage === 'failed' ? 'failed' : 'innovated';
186
196
  const cap = res.capsule;
197
+ if (deps.memoryGraph && res.decision?.selectedGeneId && (res.finalStage === 'solidified' || res.finalStage === 'failed')) {
198
+ const producedSuccess = res.finalStage === 'solidified' && res.producedValue === true;
199
+ try {
200
+ await deps.memoryGraph.recordOutcome({
201
+ workspace: task.repo,
202
+ signals: cycleSignals,
203
+ geneId: res.decision.selectedGeneId,
204
+ // MemoryGraph has no inert status yet, so record a no-op as conservative failed evidence instead of reward.
205
+ status: producedSuccess ? 'success' : 'failed',
206
+ score: res.finalStage === 'solidified' && !res.producedValue
207
+ ? 0
208
+ : cap?.outcome?.score ?? (producedSuccess ? 1 : 0),
209
+ at: new Date().toISOString(),
210
+ });
211
+ }
212
+ catch {
213
+ // Memory persistence is advisory and must never fail the autonomous task.
214
+ }
215
+ }
187
216
  const hubAssetIds = new Set(hubCandidates.map((c) => c.assetId).filter((id) => typeof id === 'string' && id.length > 0));
188
217
  const selectedAssetId = res.decision?.selectedAssetId;
189
218
  const usedAssetIds = selectedAssetId && hubAssetIds.has(selectedAssetId) ? [selectedAssetId] : [];
@@ -192,6 +221,8 @@ export async function runAutoExecTask(deps, rawTask, safety) {
192
221
  ...(res.reasons.length > 0 && status !== 'solidified' ? { reason: res.reasons.join('; ') } : {}),
193
222
  ...(cap?.outcome ? { outcome: cap.outcome } : {}),
194
223
  ...(cap?.proof_of_work ? { proofOfWork: cap.proof_of_work } : {}),
224
+ ...(res.failureKind !== undefined ? { failureKind: res.failureKind } : {}),
225
+ ...(res.exitCode !== undefined ? { exitCode: res.exitCode } : {}),
195
226
  ...(usedAssetIds.length > 0 ? { usedAssetIds } : {}),
196
227
  };
197
228
  }
@@ -29,6 +29,8 @@ export interface AutonomousSafety {
29
29
  /** Only embed trusted gene strategies (#45). Default true. */
30
30
  requireTrustedGene?: boolean;
31
31
  timeoutMs?: number;
32
+ /** Cooperative cancellation propagated to the spawned runner process tree. */
33
+ signal?: AbortSignal;
32
34
  }
33
35
  /**
34
36
  * Build the fully-hardened `execute` for an autonomous run against `repo`. Composes every exec-bridge control
@@ -43,12 +43,16 @@ const CODEX_DEFAULT_AGENT_OPTIONS = {};
43
43
  // auto-approve shell+write, but Cursor has no verified per-run allowlist/sandbox mapping yet, so skipPermissions
44
44
  // is refused outright. Use default cursor with worktree isolation until the CLI is run-verified.
45
45
  const CURSOR_DEFAULT_AGENT_OPTIONS = {};
46
+ // Gemini's verified safe default is `--approval-mode auto_edit`; shell remains gated and --yolo is refused.
47
+ const GEMINI_DEFAULT_AGENT_OPTIONS = {};
46
48
  /** Per-runner safe default agent options — claude bypasses-with-bounds; codex + cursor stay non-bypassing (codex sandboxed; cursor skip refused, #66). */
47
49
  function defaultAgentOptions(runner) {
48
50
  if (runner === 'codex')
49
51
  return CODEX_DEFAULT_AGENT_OPTIONS;
50
52
  if (runner === 'cursor')
51
53
  return CURSOR_DEFAULT_AGENT_OPTIONS;
54
+ if (runner === 'gemini')
55
+ return GEMINI_DEFAULT_AGENT_OPTIONS;
52
56
  return CLAUDE_DEFAULT_AGENT_OPTIONS;
53
57
  }
54
58
  /**
@@ -70,6 +74,7 @@ export function makeSafeExecute(repo, store, safety, opts = {}) {
70
74
  requireTrustedGene: safety.requireTrustedGene ?? true,
71
75
  agentOptions: safety.agentOptions ?? defaultAgentOptions(safety.runner),
72
76
  ...(safety.timeoutMs !== undefined ? { timeoutMs: safety.timeoutMs } : {}),
77
+ ...(safety.signal ? { signal: safety.signal } : {}),
73
78
  resolveGene: makeTrustedGeneResolver(store, opts.provenance, opts.review, opts.includeProbation ?? false),
74
79
  ...(opts.validate ? { validate: opts.validate } : {}),
75
80
  ...(opts.validationCmds ? { validationCmds: opts.validationCmds } : {}),
@@ -4,10 +4,13 @@ import type { ExecutionResult } from '../algo/cycleEngine.js';
4
4
  import { type GeneStrategyInfo } from './prompt.js';
5
5
  import type { PersonalityStore } from '../personality/store.js';
6
6
  import { type AgentRunner, type AgentRunnerOptions, type RunnerName } from './runnerRegistry.js';
7
- export { resolveSpawnCommand, spawnCapture, UnboundedSkipPermissionsError, UnsupportedCursorSkipPermissionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, } from './runnerRegistry.js';
7
+ export { resolveSpawnCommand, spawnCapture, UnboundedSkipPermissionsError, UnsupportedCursorSkipPermissionsError, UnsupportedGeminiPermissionOptionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, geminiRunnerArgs, makeGeminiHeadlessRunner, } from './runnerRegistry.js';
8
8
  export type { AgentRunContext, AgentRunResult, AgentRunner, RunnerName, AgentRunnerOptions, ClaudeRunnerOptions, CodexRunnerOptions, AgentRunnerSpec, } from './runnerRegistry.js';
9
+ export interface GitRunnerOptions {
10
+ processSignalMode?: 'cancel' | 'ignore';
11
+ }
9
12
  /** Run a git subcommand in cwd and return its stdout. */
10
- export type GitRunner = (args: readonly string[], cwd: string) => Promise<string>;
13
+ export type GitRunner = (args: readonly string[], cwd: string, signal?: AbortSignal, options?: GitRunnerOptions) => Promise<string>;
11
14
  /** Resolve the selected gene's learned strategy (for prompt enrichment). */
12
15
  export type GeneResolver = (geneId: string) => Promise<GeneStrategyInfo | null> | GeneStrategyInfo | null;
13
16
  /** Decide success from the post-run working tree (e.g. run the gene's validation plan). */
@@ -57,6 +60,8 @@ export interface ExecBridgeOptions {
57
60
  requireTrustedGene?: boolean;
58
61
  /** Per-run agent timeout. Default 600_000ms (10 min). */
59
62
  timeoutMs?: number;
63
+ /** Cooperative cancellation propagated to the runner process tree. */
64
+ signal?: AbortSignal;
60
65
  /** Optional: enrich the prompt with the selected gene's strategy. */
61
66
  resolveGene?: GeneResolver;
62
67
  /** Optional: validation commands surfaced in the prompt's done-criteria. */