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

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 (240) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Progressive curriculum signals, adapted from V1's curriculum producer.
3
+ *
4
+ * V2 keeps the policy pure and derives its history from the append-only root event log. This avoids V1's
5
+ * separate mutable curriculum_state.json sidecar while preserving the behavior that matters to selection:
6
+ * classify recent outcomes and add at most one capability-gap target plus one frontier target.
7
+ */
8
+ const MASTERY_THRESHOLD = 0.8;
9
+ const MASTERY_MIN_ATTEMPTS = 3;
10
+ const FAILURE_THRESHOLD = 0.3;
11
+ const MIN_CLASSIFICATION_ATTEMPTS = 2;
12
+ const MAX_CURRICULUM_SIGNALS = 2;
13
+ const MAX_TARGET_CHARS = 60;
14
+ const DEFAULT_OUTCOME_WINDOW = 200;
15
+ const CAPABILITY_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,59}$/;
16
+ export const MAX_CAPABILITY_GAPS = 16;
17
+ export const CAPABILITY_GAPS_STATE_KEY = 'curriculum:capability_gaps';
18
+ const CAPABILITY_GAPS_STATE_VERSION = 1;
19
+ function record(value) {
20
+ return value && typeof value === 'object' && !Array.isArray(value)
21
+ ? value
22
+ : undefined;
23
+ }
24
+ function stringArray(value) {
25
+ if (!Array.isArray(value))
26
+ return undefined;
27
+ return value.filter((entry) => typeof entry === 'string');
28
+ }
29
+ function boundedWindow(value) {
30
+ if (!Number.isFinite(value))
31
+ return DEFAULT_OUTCOME_WINDOW;
32
+ return Math.max(1, Math.min(10_000, Math.floor(value)));
33
+ }
34
+ /** Normalize untrusted capability names before they cross adapter, persistence, or selection boundaries. */
35
+ export function normalizeCapabilityGaps(value) {
36
+ if (!Array.isArray(value))
37
+ return [];
38
+ const gaps = [];
39
+ for (const entry of value) {
40
+ if (typeof entry !== 'string')
41
+ continue;
42
+ const normalized = entry.trim().toLowerCase();
43
+ if (!CAPABILITY_NAME.test(normalized) || gaps.includes(normalized))
44
+ continue;
45
+ gaps.push(normalized);
46
+ if (gaps.length >= MAX_CAPABILITY_GAPS)
47
+ break;
48
+ }
49
+ return gaps;
50
+ }
51
+ /** Versioned, bounded snapshot stored atomically in the lifecycle mailbox KV table. */
52
+ export function serializeCapabilityGapsState(capabilityGaps, observedAt) {
53
+ const safeObservedAt = Number.isFinite(observedAt) && observedAt >= 0 ? Math.floor(observedAt) : 0;
54
+ return JSON.stringify({
55
+ version: CAPABILITY_GAPS_STATE_VERSION,
56
+ capabilityGaps: normalizeCapabilityGaps(capabilityGaps),
57
+ observedAt: safeObservedAt,
58
+ });
59
+ }
60
+ /** Parse only the current bounded snapshot format. Unknown versions fail open without influencing selection. */
61
+ export function capabilityGapsFromState(value) {
62
+ if (typeof value !== 'string' || value.length === 0)
63
+ return [];
64
+ try {
65
+ const parsed = record(JSON.parse(value));
66
+ if (!parsed || parsed['version'] !== CAPABILITY_GAPS_STATE_VERSION)
67
+ return [];
68
+ const observedAt = parsed['observedAt'];
69
+ if (typeof observedAt !== 'number' || !Number.isFinite(observedAt) || observedAt < 0)
70
+ return [];
71
+ return normalizeCapabilityGaps(parsed['capabilityGaps']);
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ /** Stable V2 equivalent of V1 computeSignalKey, excluding previously generated curriculum targets. */
78
+ export function curriculumSignalKey(signals) {
79
+ return [...new Set(signals
80
+ .map((signal) => signal.trim())
81
+ .filter((signal) => signal.length > 0 && !signal.startsWith('curriculum_target:')))]
82
+ .sort()
83
+ .join('|');
84
+ }
85
+ /** Classify signal-key outcomes using V1's thresholds. */
86
+ export function classifyCurriculumOutcomes(outcomes) {
87
+ const aggregates = new Map();
88
+ for (const outcome of outcomes) {
89
+ const key = typeof outcome?.key === 'string' ? outcome.key.trim() : '';
90
+ if (!key || (outcome.status !== 'success' && outcome.status !== 'failed'))
91
+ continue;
92
+ const aggregate = aggregates.get(key) ?? { success: 0, failed: 0 };
93
+ if (outcome.status === 'success')
94
+ aggregate.success += 1;
95
+ else
96
+ aggregate.failed += 1;
97
+ aggregates.set(key, aggregate);
98
+ }
99
+ const mastered = [];
100
+ const failing = [];
101
+ const frontier = [];
102
+ for (const [key, aggregate] of aggregates) {
103
+ const total = aggregate.success + aggregate.failed;
104
+ if (total < MIN_CLASSIFICATION_ATTEMPTS)
105
+ continue;
106
+ const rate = aggregate.success / total;
107
+ const bucket = { key, success: aggregate.success, failed: aggregate.failed, total, rate };
108
+ if (rate >= MASTERY_THRESHOLD && total >= MASTERY_MIN_ATTEMPTS)
109
+ mastered.push(bucket);
110
+ else if (rate <= FAILURE_THRESHOLD)
111
+ failing.push(bucket);
112
+ else
113
+ frontier.push(bucket);
114
+ }
115
+ frontier.sort((left, right) => Math.abs(left.rate - 0.5) - Math.abs(right.rate - 0.5));
116
+ return { mastered, failing, frontier };
117
+ }
118
+ /** Generate no more than the two target families V1 produced: gap first, then closest frontier. */
119
+ export function generateCurriculumSignals(input) {
120
+ const analysis = classifyCurriculumOutcomes(input.outcomes);
121
+ const generated = [];
122
+ const gap = (input.capabilityGaps ?? []).find((entry) => typeof entry === 'string' && entry.trim().length > 0)?.trim();
123
+ if (gap) {
124
+ const normalizedGap = gap.toLowerCase();
125
+ const alreadyMastered = analysis.mastered.some((entry) => entry.key.toLowerCase().includes(normalizedGap));
126
+ if (!alreadyMastered)
127
+ generated.push(`curriculum_target:gap:${gap.slice(0, MAX_TARGET_CHARS)}`);
128
+ }
129
+ const best = analysis.frontier[0];
130
+ if (generated.length < MAX_CURRICULUM_SIGNALS && best) {
131
+ const alreadyTargeted = generated.some((signal) => signal.includes(best.key));
132
+ if (!alreadyTargeted)
133
+ generated.push(`curriculum_target:frontier:${best.key.slice(0, MAX_TARGET_CHARS)}`);
134
+ }
135
+ return generated.slice(0, MAX_CURRICULUM_SIGNALS);
136
+ }
137
+ /**
138
+ * Map V2's capability signal conventions to concrete curriculum gaps. A bare cap:* tag is not sufficient by
139
+ * itself: it becomes a curriculum gap only when the same signal set explicitly declares capability_gap.
140
+ */
141
+ export function capabilityGapsFromSignals(signals) {
142
+ const hasGapMarker = signals.some((signal) => signal === 'capability_gap' || signal.startsWith('capability_gap:'));
143
+ if (!hasGapMarker)
144
+ return [];
145
+ const gaps = [];
146
+ const add = (raw) => {
147
+ const normalized = raw.trim().toLowerCase();
148
+ if (!CAPABILITY_NAME.test(normalized) || gaps.includes(normalized))
149
+ return;
150
+ gaps.push(normalized);
151
+ };
152
+ for (const signal of signals)
153
+ if (signal.startsWith('cap:'))
154
+ add(signal.slice('cap:'.length));
155
+ for (const signal of signals)
156
+ if (signal.startsWith('capability_gap:'))
157
+ add(signal.slice('capability_gap:'.length));
158
+ return normalizeCapabilityGaps(gaps);
159
+ }
160
+ /**
161
+ * Join cycle.signals_collected to terminal cycle events and retain the latest outcome window. `baseSignals`
162
+ * wins over `signals`, so history-derived meta-signals do not become a self-reinforcing curriculum key.
163
+ */
164
+ export function curriculumOutcomesFromEvents(events, maxOutcomes = DEFAULT_OUTCOME_WINDOW) {
165
+ const limit = boundedWindow(maxOutcomes);
166
+ const selectedNewestFirst = [];
167
+ const terminalCycles = new Set();
168
+ // Pick the latest unique terminal cycles first. EventStore.readAll already owns the input array, while every
169
+ // auxiliary collection in this adapter remains bounded by the configured outcome window.
170
+ for (let index = events.length - 1; index >= 0 && selectedNewestFirst.length < limit; index -= 1) {
171
+ const event = events[index];
172
+ if (event.type !== 'cycle.solidified' && event.type !== 'cycle.failed')
173
+ continue;
174
+ const payload = record(event.payload);
175
+ if (!payload || typeof payload['cycleId'] !== 'string' || terminalCycles.has(payload['cycleId']))
176
+ continue;
177
+ const cycleId = payload['cycleId'];
178
+ terminalCycles.add(cycleId);
179
+ selectedNewestFirst.push({
180
+ cycleId,
181
+ status: event.type === 'cycle.failed' || payload['producedValue'] === false ? 'failed' : 'success',
182
+ });
183
+ }
184
+ const signalsByCycle = new Map();
185
+ // Build the signal lookup independently of terminal ordering. Normal logs collect signals first, but a
186
+ // recovered/imported history can be reordered without making otherwise valid outcomes disappear.
187
+ for (const event of events) {
188
+ const payload = record(event.payload);
189
+ if (!payload)
190
+ continue;
191
+ const cycleId = typeof payload['cycleId'] === 'string' ? payload['cycleId'] : undefined;
192
+ if (!cycleId || !terminalCycles.has(cycleId) || event.type !== 'cycle.signals_collected')
193
+ continue;
194
+ const baseSignals = stringArray(payload['baseSignals']);
195
+ const fallbackSignals = stringArray(payload['signals']);
196
+ signalsByCycle.set(cycleId, baseSignals ?? fallbackSignals ?? []);
197
+ }
198
+ return selectedNewestFirst.reverse().flatMap(({ cycleId, status }) => {
199
+ const key = curriculumSignalKey(signalsByCycle.get(cycleId) ?? []);
200
+ return key ? [{ key, status }] : [];
201
+ });
202
+ }
@@ -8,7 +8,8 @@
8
8
  // - intent: mutation.built.payload.category (the cycle's GepCategory), if emitted
9
9
  // - genesUsed: decision.gene_selected.payload.selectedGeneId || cycle.*.payload.gene
10
10
  // - score: cycle.*.payload.outcome.score
11
- // - blastRadius:capsule.produced.payload.blastRadius, if emitted (else absent not counted as empty)
11
+ // - blastRadius:capsule.produced.payload.blastRadius, if emitted. Productive {0,0} is omitted because non-git
12
+ // proofs use it as "unknown"; measured/no-value {0,0} remains an empty-cycle marker.
12
13
  // Only terminal cycles (a cycle.solidified or cycle.failed) become records; in-flight cycles are skipped.
13
14
  function payloadObj(e) {
14
15
  return e.payload && typeof e.payload === 'object' ? e.payload : {};
@@ -22,13 +23,12 @@ function cycleIdOf(p) {
22
23
  */
23
24
  export function cycleRecordsFromEvents(events) {
24
25
  const byCycle = new Map();
25
- const order = [];
26
+ const terminalOrder = [];
26
27
  const get = (id) => {
27
28
  let a = byCycle.get(id);
28
29
  if (!a) {
29
30
  a = { terminal: false };
30
31
  byCycle.set(id, a);
31
- order.push(id);
32
32
  }
33
33
  return a;
34
34
  };
@@ -53,15 +53,24 @@ export function cycleRecordsFromEvents(events) {
53
53
  const br = p['blastRadius'];
54
54
  if (br && typeof br === 'object') {
55
55
  const o = br;
56
- a.blastRadius = {
57
- ...(typeof o['files'] === 'number' ? { files: o['files'] } : {}),
58
- ...(typeof o['lines'] === 'number' ? { lines: o['lines'] } : {}),
59
- };
56
+ const files = typeof o['files'] === 'number' ? o['files'] : undefined;
57
+ const lines = typeof o['lines'] === 'number' ? o['lines'] : undefined;
58
+ // Non-git proofs use {0,0} as "blast unknown". producedValue is the authoritative distinction between
59
+ // those productive cycles and a measured zero-change git cycle.
60
+ const productiveUnknownBlast = p['producedValue'] === true && files === 0 && lines === 0;
61
+ if (!productiveUnknownBlast) {
62
+ a.blastRadius = {
63
+ ...(files !== undefined ? { files } : {}),
64
+ ...(lines !== undefined ? { lines } : {}),
65
+ };
66
+ }
60
67
  }
61
68
  break;
62
69
  }
63
70
  case 'cycle.solidified':
64
71
  case 'cycle.failed': {
72
+ if (!a.terminal)
73
+ terminalOrder.push(id);
65
74
  a.terminal = true;
66
75
  a.status = e.type === 'cycle.solidified' ? 'success' : 'failed';
67
76
  if (typeof p['gene'] === 'string')
@@ -77,7 +86,7 @@ export function cycleRecordsFromEvents(events) {
77
86
  }
78
87
  }
79
88
  const records = [];
80
- for (const id of order) {
89
+ for (const id of terminalOrder) {
81
90
  const a = byCycle.get(id);
82
91
  if (!a.terminal)
83
92
  continue; // skip in-flight cycles
@@ -31,4 +31,18 @@ export declare function geneTags(gene: GeneTagInput): string[];
31
31
  * literal=1.0 upstream in expandedMatchScore. v1 #99 gave CJK full literal credit via pipe-aliases; v2 reaches
32
32
  * the right gene at ~2/3 strength. Lifting this to parity is a scoring-design change beyond this signal port.
33
33
  */
34
- export declare function tagOverlapScore(signals: readonly string[], gene: GeneTagInput): number;
34
+ export declare function tagOverlapScore(signals: readonly string[], gene: GeneTagInput): number;
35
+ export interface SemanticIdfDocument {
36
+ readonly tags: readonly string[];
37
+ readonly text: string;
38
+ }
39
+ export interface SemanticIdfProfile {
40
+ readonly documentCount: number;
41
+ readonly tagIdf: ReadonlyMap<string, number>;
42
+ readonly tokenIdf: ReadonlyMap<string, number>;
43
+ /** Deterministic, data-free identity for replaying the exact document-frequency profile. */
44
+ readonly version: string;
45
+ }
46
+ export declare function buildSemanticIdfProfile(documents: readonly SemanticIdfDocument[]): SemanticIdfProfile;
47
+ export declare function idfTagOverlapScore(signals: readonly string[], gene: GeneTagInput, profile: SemanticIdfProfile): number;
48
+ export declare function idfBagCosine(a: string, b: string, profile: SemanticIdfProfile): number;
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  // Signal expansion & tag classification (ported from v1 src/gep/learningSignals.js).
2
3
  // Raw signals (e.g. '429') are expanded into broader semantic tags (e.g. 'problem:reliability',
3
4
  // 'action:repair') so gene selection can match by MEANING, not just literal string intersection —
@@ -8,6 +9,18 @@
8
9
  // reach an English-keyworded gene through tagOverlapScore. This is the v2-native port of v1 #99's
9
10
  // multilingual signals_match aliases: v1 attached `|`-pipe synonyms to shipped seed genes, but v2 has
10
11
  // no seed-gene catalog and no pipe-alias parsing — its expansion rules ARE the shared signal vocabulary.
12
+ const SUCCESS_OUTCOME_TAGS = ['signal:success', 'action:optimize', 'action:innovate'];
13
+ const EXPLICIT_SUCCESS_SIGNALS = new Set([
14
+ 'issue_already_resolved',
15
+ 'issue_resolved',
16
+ 'openclaw_self_healed',
17
+ 'resolved',
18
+ 'self_healed',
19
+ 'stable_success_plateau',
20
+ 'success_prose',
21
+ 'verified-success',
22
+ 'verified_success',
23
+ ]);
11
24
  const EXPANSION_RULES = [
12
25
  { re: /(error|exception|failed|unstable|log_error|runtime|429|错误|异常|エラー|오류|例外|예외|失败|失敗|실패|不稳定|不安定|불안정)/, tags: ['problem:reliability', 'action:repair'] },
13
26
  // Korean 감사 alone is a homograph (audit == "thank you"), so it is matched only via compounds / audit-context
@@ -19,13 +32,28 @@ const EXPANSION_RULES = [
19
32
  // 功能/機能/기능 alone are too broad: as unanchored substrings they match the malfunction vocabulary
20
33
  // (機能不全 / 기능장애 / 功能障碍), which are reliability problems — use the v1 #99 feature-request compounds.
21
34
  { re: /(feature|capability_gap|user_feature_request|external_opportunity|stagnation recommendation|功能请求|機能リクエスト|기능요청|能力缺口|機能ギャップ|역량공백|改进建议|改善提案|개선제안|外部机会|外部機会|외부기회)/, tags: ['problem:capability', 'action:innovate'] },
22
- { re: /(stagnation|plateau|steady_state|saturation|empty_cycle_loop|loop_detected|recurring)/, tags: ['problem:stagnation', 'action:innovate'] },
35
+ // Negative lookbehind on `plateau`: `stable_success_plateau` contains the substring `plateau`, but it is a
36
+ // SUCCESS signal (#578), not a stagnation signal. Without (?<!success_) the stagnation rule would fire on it,
37
+ // producing contradictory tags (problem:stagnation + signal:success) in a single expandSignals pass.
38
+ { re: /(stagnation|(?<!success_)plateau|steady_state|saturation|empty_cycle_loop|loop_detected|recurring)/, tags: ['problem:stagnation', 'action:innovate'] },
23
39
  { re: /(task|worker|heartbeat|hub|commitment|assignment|orchestration)/, tags: ['area:orchestration'] },
24
40
  // Tool-integrity (ported from v1 #99 gene_tool_integrity): bypassing a registered tool or looping on raw
25
41
  // shell is an orchestration-discipline / validation risk. CN/JA/KO aliases included for recall parity.
26
42
  { re: /(tool_bypass|tool_loop|工具绕过|工具循环|ツール迂回|ツールループ|도구우회|도구반복)/, tags: ['area:orchestration', 'risk:validation'] },
27
43
  { re: /(memory|narrative|reflection)/, tags: ['area:memory'] },
28
44
  { re: /(skill|dashboard)/, tags: ['area:skills'] },
45
+ // Harness context budget (v1 context-compression gene family port): an agent harness that injects large
46
+ // tool/MCP schemas, long skill manuals, agent-type descriptions, a memory index, or a pasted transcript can
47
+ // exhaust the model's context window before the task starts. That is a prompt-assembly (area:prompt) cost
48
+ // problem, and shrinking it is an optimize action — NOT a reliability failure. v2 has no seed-gene catalog,
49
+ // so these rules ARE the shared vocabulary that lets a context-bloat signal reach a compression strategy.
50
+ { re: /(context_bloat|context_explosion|context window|token_budget|prompt_budget|上下文爆|上下文预算|token 超限|token超限)/, tags: ['problem:context_budget', 'action:optimize', 'area:prompt'] },
51
+ // Tool/MCP schema weight is the always-on half of the budget; lazy-loading it is the corrective action.
52
+ { re: /(tool_schema|schema_bloat|mcp_tool_schema|lazy_load_schema|just-in-time schema|工具 schema|工具schema)/, tags: ['problem:context_budget', 'action:optimize', 'area:prompt', 'area:orchestration'] },
53
+ // Skill manuals / agent-type descriptions are the other always-on half (area:skills already exists above).
54
+ { re: /(skill_list_bloat|skill_manual_bloat|agent type description|mcp server instructions|skill 列表太长|技能列表太长)/, tags: ['problem:context_budget', 'action:optimize', 'area:skills', 'area:prompt'] },
55
+ // A pasted transcript / session handoff is the caller-supplied half — compressing it is a memory-shaped concern.
56
+ { re: /(transcript_bloat|transcript_context_bloat|conversation_handoff|session handoff|pasted transcript|会话上下文就爆|完整转录)/, tags: ['problem:context_budget', 'action:optimize', 'area:memory'] },
29
57
  { re: /(validation|canary|rollback|constraint|blast radius|destructive)/, tags: ['risk:validation'] },
30
58
  ];
31
59
  /**
@@ -41,6 +69,11 @@ export function expandSignals(signals, extraText = '') {
41
69
  if (base && base !== str)
42
70
  tags.add(base); // namespace prefix (e.g. 'auth:token' → 'auth')
43
71
  }
72
+ // #578 success markers are structured signals; prose inference accepts negated forms too easily.
73
+ if (signals.some((signal) => EXPLICIT_SUCCESS_SIGNALS.has(String(signal).trim().toLowerCase().normalize('NFKC')))) {
74
+ for (const tag of SUCCESS_OUTCOME_TAGS)
75
+ tags.add(tag);
76
+ }
44
77
  const text = (signals.join(' ') + ' ' + extraText).toLowerCase().normalize('NFKC');
45
78
  for (const rule of EXPANSION_RULES) {
46
79
  if (rule.re.test(text))
@@ -122,4 +155,139 @@ export function tagOverlapScore(signals, gene) {
122
155
  if (geneTagSet.has(t))
123
156
  hits++;
124
157
  return hits / sigTags.length;
158
+ }
159
+ const SEMANTIC_IDF_DOCUMENT_LIMIT = 1_000;
160
+ const SEMANTIC_IDF_TEXT_CHARS_PER_DOCUMENT = 4_096;
161
+ const SEMANTIC_IDF_TAG_INPUTS_PER_DOCUMENT = 256;
162
+ const SEMANTIC_IDF_TAG_CHARS = 1_024;
163
+ const SEMANTIC_IDF_TAGS_PER_DOCUMENT = 64;
164
+ const SEMANTIC_IDF_TOKENS_PER_DOCUMENT = 128;
165
+ const SEMANTIC_IDF_TAG_VOCABULARY_LIMIT = 8_192;
166
+ const SEMANTIC_IDF_TOKEN_VOCABULARY_LIMIT = 32_768;
167
+ function normalizeSemanticTag(tag) {
168
+ return tag.normalize('NFKC').trim().toLowerCase();
169
+ }
170
+ function inverseDocumentFrequency(documentCount, documentFrequency) {
171
+ return Math.log(documentCount / documentFrequency);
172
+ }
173
+ function tokenFrequency(text) {
174
+ const frequencies = new Map();
175
+ for (const token of tokenize(text)) {
176
+ frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
177
+ }
178
+ return frequencies;
179
+ }
180
+ function compareCodeUnits(left, right) {
181
+ return left < right ? -1 : left > right ? 1 : 0;
182
+ }
183
+ function boundedUnique(values, limit) {
184
+ return [...new Set(values.filter(Boolean))].sort(compareCodeUnits).slice(0, limit);
185
+ }
186
+ function boundedVocabulary(valuesByDocument, limit) {
187
+ const vocabulary = new Set();
188
+ for (const values of valuesByDocument) {
189
+ for (const value of values)
190
+ vocabulary.add(value);
191
+ }
192
+ // Selecting by code-unit order makes the cap independent of corpus/document order. The source set is itself
193
+ // bounded by the per-document and document-count limits above, so this sort cannot grow without bound.
194
+ return new Set([...vocabulary].sort(compareCodeUnits).slice(0, limit));
195
+ }
196
+ function semanticProfileVersion(documentCount, tagDocumentFrequency, tokenDocumentFrequency) {
197
+ // Hash only normalized document-frequency entries so decision telemetry never contains plaintext corpus data.
198
+ // A 128-bit SHA-256 prefix keeps the identifier compact while avoiding the collision rate of a 32-bit checksum.
199
+ const hash = createHash('sha256');
200
+ hash.update(`documents:${documentCount};`, 'utf8');
201
+ for (const [tag, frequency] of [...tagDocumentFrequency].sort(([left], [right]) => compareCodeUnits(left, right))) {
202
+ hash.update(`tag:${tag.length}:${tag}:${frequency};`, 'utf8');
203
+ }
204
+ for (const [token, frequency] of [...tokenDocumentFrequency].sort(([left], [right]) => compareCodeUnits(left, right))) {
205
+ hash.update(`token:${token.length}:${token}:${frequency};`, 'utf8');
206
+ }
207
+ return `idf-2:n=${documentCount};tags=${tagDocumentFrequency.size};tokens=${tokenDocumentFrequency.size};sha256=${hash.digest('hex').slice(0, 32)}`;
208
+ }
209
+ export function buildSemanticIdfProfile(documents) {
210
+ const boundedDocuments = documents.slice(0, SEMANTIC_IDF_DOCUMENT_LIMIT).map((document) => ({
211
+ tags: boundedUnique(document.tags
212
+ .slice(0, SEMANTIC_IDF_TAG_INPUTS_PER_DOCUMENT)
213
+ .map((tag) => normalizeSemanticTag(tag.slice(0, SEMANTIC_IDF_TAG_CHARS))), SEMANTIC_IDF_TAGS_PER_DOCUMENT),
214
+ tokens: boundedUnique(tokenize(document.text.slice(0, SEMANTIC_IDF_TEXT_CHARS_PER_DOCUMENT)), SEMANTIC_IDF_TOKENS_PER_DOCUMENT),
215
+ }));
216
+ const tagVocabulary = boundedVocabulary(boundedDocuments.map((document) => document.tags), SEMANTIC_IDF_TAG_VOCABULARY_LIMIT);
217
+ const tokenVocabulary = boundedVocabulary(boundedDocuments.map((document) => document.tokens), SEMANTIC_IDF_TOKEN_VOCABULARY_LIMIT);
218
+ const tagDocumentFrequency = new Map();
219
+ const tokenDocumentFrequency = new Map();
220
+ for (const document of boundedDocuments) {
221
+ for (const tag of document.tags) {
222
+ if (!tagVocabulary.has(tag))
223
+ continue;
224
+ tagDocumentFrequency.set(tag, (tagDocumentFrequency.get(tag) ?? 0) + 1);
225
+ }
226
+ for (const token of document.tokens) {
227
+ if (!tokenVocabulary.has(token))
228
+ continue;
229
+ tokenDocumentFrequency.set(token, (tokenDocumentFrequency.get(token) ?? 0) + 1);
230
+ }
231
+ }
232
+ const documentCount = boundedDocuments.length;
233
+ const tagIdf = new Map();
234
+ const tokenIdf = new Map();
235
+ for (const [tag, frequency] of tagDocumentFrequency) {
236
+ tagIdf.set(tag, inverseDocumentFrequency(documentCount, frequency));
237
+ }
238
+ for (const [token, frequency] of tokenDocumentFrequency) {
239
+ tokenIdf.set(token, inverseDocumentFrequency(documentCount, frequency));
240
+ }
241
+ return {
242
+ documentCount,
243
+ tagIdf,
244
+ tokenIdf,
245
+ version: semanticProfileVersion(documentCount, tagDocumentFrequency, tokenDocumentFrequency),
246
+ };
247
+ }
248
+ export function idfTagOverlapScore(signals, gene, profile) {
249
+ if (profile.documentCount <= 1)
250
+ return tagOverlapScore(signals, gene);
251
+ const signalTags = [...new Set(expandSignals(signals).map(normalizeSemanticTag))];
252
+ const geneTagSet = new Set(geneTags(gene).map(normalizeSemanticTag));
253
+ let matchedWeight = 0;
254
+ let totalWeight = 0;
255
+ for (const tag of signalTags) {
256
+ const weight = profile.tagIdf.get(tag);
257
+ if (weight === undefined)
258
+ continue;
259
+ totalWeight += weight;
260
+ if (geneTagSet.has(tag))
261
+ matchedWeight += weight;
262
+ }
263
+ return totalWeight > 0 ? matchedWeight / totalWeight : 0;
264
+ }
265
+ export function idfBagCosine(a, b, profile) {
266
+ if (profile.documentCount <= 1)
267
+ return bagCosine(a, b);
268
+ const bagA = tokenFrequency(a);
269
+ const bagB = tokenFrequency(b);
270
+ let dot = 0;
271
+ let normA = 0;
272
+ let normB = 0;
273
+ for (const [token, count] of bagA) {
274
+ const weight = profile.tokenIdf.get(token);
275
+ if (weight === undefined)
276
+ continue;
277
+ const weightedCount = count * weight;
278
+ normA += weightedCount * weightedCount;
279
+ const otherCount = bagB.get(token);
280
+ if (otherCount !== undefined)
281
+ dot += weightedCount * otherCount * weight;
282
+ }
283
+ for (const [token, count] of bagB) {
284
+ const weight = profile.tokenIdf.get(token);
285
+ if (weight === undefined)
286
+ continue;
287
+ const weightedCount = count * weight;
288
+ normB += weightedCount * weightedCount;
289
+ }
290
+ if (normA === 0 || normB === 0)
291
+ return 0;
292
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
125
293
  }
@@ -6,8 +6,8 @@ export interface SignalSourceTurn {
6
6
  errorMessage?: string;
7
7
  isMeta?: boolean;
8
8
  }
9
- /** 信号强度三条腿(批注#13): strong=零成本结构化判断 / agent=自标记 / weak=需 LLM 分析. */
10
- export type SignalStrength = 'strong' | 'agent' | 'weak';
9
+ /** 信号强度四条腿(批注#13): strong=零成本结构化错误判断 / agent=自标记 / weak=需 LLM 分析 / success=成功信号(Issue#578). */
10
+ export type SignalStrength = 'strong' | 'agent' | 'weak' | 'success';
11
11
  export interface ExtractedSignal {
12
12
  id: string;
13
13
  strength: SignalStrength;
@@ -4,7 +4,16 @@ const AGENT_MARKERS = [/\bEVOLVE_SIGNAL:\s*(.+)/i, /\[SIGNAL\]\s*(.+)/i];
4
4
  /** 结构化强信号: 栈/退出码/显式 Error — 零成本判断, 不动 LLM. */
5
5
  const STRONG_TEXT = /(^|\n)\s*(Error:|Traceback|Exception|FAILED|panic:|\bexit code [1-9])/;
6
6
  /** 弱信号: 表达困难但无结构, 需 LLM 拼语境. */
7
- const DIFFICULTY = /(无法|失败|搞不定|stuck|can'?t|unable to|not working|报错|卡住)/i;
7
+ const DIFFICULTY = /(无法|失败|搞不定|stuck|can'?t|unable to|not working|fail(?:ed|ing|ure)?|broken|did not pass|no tests? passed|报错|卡住)/i;
8
+ /** 成功信号: 工具执行成功/测试通过/构建成功 — 零成本结构化判断, 不动 LLM(Issue#578).
9
+ * 注意: bare `verified` 和 `green` 已移至 SUCCESS_PROSE(needsAnalysis:true), 因为它们在非成功语境下高频出现
10
+ * ("verified the bug exists" / "green field" / "green button") 且 SUCCESS_TEXT 的 needsAnalysis:false 无 LLM 兜底.
11
+ * Emoji are accepted only with test-result context; a bare checklist mark is not proof of session success. */
12
+ const SUCCESS_TEXT = /\b(exit code:?\s*0|all tests?\s+pass(ed)?|tests?\s+pass(ed)?|validation\s+pass(ed)?|build\s+succeed(ed)?|published\s+successfully|completed\s+successfully)\b|(?:^|\n)\s*(?:✓\s*(?:\d+\s+)?(?:tests?\s+)?pass(ed)?\b|(?:Tests|Test Files):?\s+[1-9]\d*\s+pass(ed)?\b|(?:=+\s*)?[1-9]\d*\s+pass(ed)?(?:\s+in\s+\d+(?:\.\d+)?s?|\s*=*\s*$)|PASS\s+\S+|ok\s+\S+\s+(?:\d+(?:\.\d+)?s|\(cached\))(?:\s|$))|\btests?\s+🟢(?:\s|$)/im;
13
+ /** Deterministic success must fail closed when the same result carries a current negative outcome. */
14
+ const SUCCESS_CONFLICT = /\b(?:no|zero|0)(?:\s+of\s+\d+)?\s+tests?\s+pass(ed)?\b|\bsome\s+tests?\s+pass(ed)?\b|\b(?:tests?|validation|verification|build)\s+(?:fail(?:ed|ing|ure)?|did\s+not\s+pass)\b|\b\d+\s+(?:tests?\s+)?failed\b|\b(?:implementation\s+)?(?:still\s+|remains?\s+)(?:broken|failing|not\s+working)\b|\b(?:not|never|did\s+not|has\s+not|hasn't|have\s+not|haven't)\s+(?:succeed(ed)?|complete(d)?\s+successfully)\b/i;
15
+ /** 成功措辞(非结构化但表达完成/解决): 需上下文确认(Issue#578). bare `verified` 也在此层: "verified the fix" vs "verified the bug" 需 LLM 判断. */
16
+ const SUCCESS_PROSE = /\b(successful(?:ly)?|verified|resolved|fixed|working\s+now|works\s+correctly|problem\s+solved|issue\s+resolved|done|complete)\b/i;
8
17
  /**
9
18
  * Harness-coordination noise — agent/tool mechanics, NOT engineering problems to evolve genes for.
10
19
  * Observation showed these dominate the "strong" leg (tool errors) and drown out real problems: the agent
@@ -33,7 +42,8 @@ export function extractSignals(turns) {
33
42
  continue;
34
43
  }
35
44
  const text = t.text ?? '';
36
- if (!text.trim())
45
+ const toolResult = t.toolResult ?? '';
46
+ if (!text.trim() && !toolResult.trim())
37
47
  continue;
38
48
  // 腿3 agent: 显式标记
39
49
  const marker = AGENT_MARKERS.map((re) => text.match(re)).find(Boolean);
@@ -41,16 +51,31 @@ export function extractSignals(turns) {
41
51
  out.push({ id: makeUlid(), strength: 'agent', kind: 'agent_marked', text: (marker[1] ?? text).trim().slice(0, 2000), needsAnalysis: false });
42
52
  continue;
43
53
  }
44
- // 1 strong: 结构化 Error 文本(同样滤掉 harness 噪声)
45
- if (STRONG_TEXT.test(text)) {
46
- if (!isHarnessNoise(text)) {
47
- out.push({ id: makeUlid(), strength: 'strong', kind: 'structured_error', text: text.slice(0, 2000), needsAnalysis: false });
54
+ // Leg 1 strong: inspect both fields. Some adapters populate only toolResult, while older callers may populate
55
+ // text as well; a failure in either representation must outrank a positive substring in the other.
56
+ const resultText = toolResult.trim() ? toolResult : text;
57
+ const structuredFailureText = STRONG_TEXT.test(text) ? text : (STRONG_TEXT.test(toolResult) ? toolResult : '');
58
+ if (structuredFailureText) {
59
+ if (!isHarnessNoise(structuredFailureText)) {
60
+ out.push({ id: makeUlid(), strength: 'strong', kind: 'structured_error', text: structuredFailureText.slice(0, 2000), ...(t.toolName ? { toolName: t.toolName } : {}), needsAnalysis: false });
61
+ }
62
+ continue;
63
+ }
64
+ // Leg 4 success: prefer the actual tool result; adapters intentionally keep tool-turn text empty.
65
+ if (SUCCESS_TEXT.test(resultText) && !SUCCESS_CONFLICT.test(resultText)) {
66
+ if (!isHarnessNoise(resultText)) {
67
+ out.push({ id: makeUlid(), strength: 'success', kind: 'verified_success', text: resultText.slice(0, 2000), ...(t.toolName ? { toolName: t.toolName } : {}), needsAnalysis: false });
48
68
  }
49
69
  continue;
50
70
  }
51
71
  // 腿2 weak: 困难措辞无结构 → 交 LLM
52
72
  if (DIFFICULTY.test(text)) {
53
73
  out.push({ id: makeUlid(), strength: 'weak', kind: 'difficulty', text: text.slice(0, 2000), needsAnalysis: true });
74
+ continue;
75
+ }
76
+ // 腿4 success: 非结构化成功措辞(resolved/fixed/working now 等) → 需上下文确认(Issue#578)
77
+ if (SUCCESS_PROSE.test(text)) {
78
+ out.push({ id: makeUlid(), strength: 'success', kind: 'success_prose', text: text.slice(0, 2000), needsAnalysis: true });
54
79
  }
55
80
  }
56
81
  return out;
@@ -3,4 +3,7 @@ export * from './signalGate.js';
3
3
  export * from './expand.js';
4
4
  export * from './traceSignals.js';
5
5
  export * from './metaSignals.js';
6
- export * from './cycleHistoryFromEvents.js';
6
+ export * from './cycleHistoryFromEvents.js';
7
+ export * from './curriculum.js';
8
+ export * from './taskDomain.js';
9
+ export * from './scopeVocabulary.js';
@@ -3,4 +3,7 @@ export * from './signalGate.js';
3
3
  export * from './expand.js';
4
4
  export * from './traceSignals.js';
5
5
  export * from './metaSignals.js';
6
- export * from './cycleHistoryFromEvents.js';
6
+ export * from './cycleHistoryFromEvents.js';
7
+ export * from './curriculum.js';
8
+ export * from './taskDomain.js';
9
+ export * from './scopeVocabulary.js';
@@ -8,6 +8,10 @@ export interface CycleHistory {
8
8
  consecutiveEmptyCycles: number;
9
9
  /** Trailing run of consecutive failed cycles at the tail. */
10
10
  consecutiveFailureCount: number;
11
+ /** Trailing run of consecutive successful cycles at the tail. */
12
+ consecutiveSuccessCount: number;
13
+ /** Successful cycles within the recent window. */
14
+ successCycleCount: number;
11
15
  /** Fraction of the recent window that failed (0..1). */
12
16
  recentFailureRatio: number;
13
17
  /** geneId → times used within the recent window (informs which gene dominates a failure loop). */