synergyspec-selfevolving 1.4.0 → 2.1.0

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 (93) hide show
  1. package/README.md +31 -18
  2. package/dist/commands/learn.d.ts +12 -1
  3. package/dist/commands/learn.js +158 -11
  4. package/dist/commands/self-evolution-episode.d.ts +177 -0
  5. package/dist/commands/self-evolution-episode.js +431 -0
  6. package/dist/commands/self-evolution.d.ts +12 -190
  7. package/dist/commands/self-evolution.js +114 -866
  8. package/dist/core/archive.d.ts +0 -1
  9. package/dist/core/archive.js +0 -58
  10. package/dist/core/artifact-graph/instruction-loader.d.ts +2 -4
  11. package/dist/core/artifact-graph/instruction-loader.js +3 -31
  12. package/dist/core/fitness/loss.d.ts +5 -5
  13. package/dist/core/fitness/loss.js +4 -4
  14. package/dist/core/fitness/test-failures.js +10 -2
  15. package/dist/core/project-config.d.ts +19 -0
  16. package/dist/core/project-config.js +96 -0
  17. package/dist/core/self-evolution/candidate-fitness.d.ts +23 -1
  18. package/dist/core/self-evolution/candidate-fitness.js +31 -5
  19. package/dist/core/self-evolution/candidates.d.ts +0 -9
  20. package/dist/core/self-evolution/critic-agent.d.ts +192 -0
  21. package/dist/core/self-evolution/critic-agent.js +568 -0
  22. package/dist/core/self-evolution/edits-contract.d.ts +53 -0
  23. package/dist/core/self-evolution/edits-contract.js +89 -0
  24. package/dist/core/self-evolution/episode-orchestrator.d.ts +234 -0
  25. package/dist/core/self-evolution/episode-orchestrator.js +681 -0
  26. package/dist/core/self-evolution/episode-store.d.ts +266 -0
  27. package/dist/core/self-evolution/episode-store.js +573 -0
  28. package/dist/core/self-evolution/evolution-switches.d.ts +1 -1
  29. package/dist/core/self-evolution/evolution-switches.js +5 -10
  30. package/dist/core/self-evolution/evolving-agent.d.ts +208 -0
  31. package/dist/core/self-evolution/evolving-agent.js +535 -0
  32. package/dist/core/self-evolution/host-harness.d.ts +14 -15
  33. package/dist/core/self-evolution/host-harness.js +48 -23
  34. package/dist/core/self-evolution/index.d.ts +11 -6
  35. package/dist/core/self-evolution/index.js +20 -6
  36. package/dist/core/self-evolution/line-diff.d.ts +60 -0
  37. package/dist/core/self-evolution/line-diff.js +130 -0
  38. package/dist/core/self-evolution/policy/fs-safe.d.ts +19 -0
  39. package/dist/core/self-evolution/policy/fs-safe.js +89 -0
  40. package/dist/core/self-evolution/policy/index.d.ts +13 -0
  41. package/dist/core/self-evolution/policy/index.js +13 -0
  42. package/dist/core/self-evolution/policy/policy-store.d.ts +217 -0
  43. package/dist/core/self-evolution/policy/policy-store.js +774 -0
  44. package/dist/core/self-evolution/policy/prediction-reconcile.d.ts +54 -0
  45. package/dist/core/self-evolution/policy/prediction-reconcile.js +191 -0
  46. package/dist/core/self-evolution/policy/reject-buffer.d.ts +55 -0
  47. package/dist/core/self-evolution/policy/reject-buffer.js +170 -0
  48. package/dist/core/self-evolution/promote.d.ts +1 -1
  49. package/dist/core/self-evolution/promote.js +6 -33
  50. package/dist/core/self-evolution/promotion.js +1 -2
  51. package/dist/core/self-evolution/reward-agent.d.ts +379 -0
  52. package/dist/core/self-evolution/reward-agent.js +940 -0
  53. package/dist/core/self-evolution/reward-aggregator.d.ts +59 -0
  54. package/dist/core/self-evolution/reward-aggregator.js +262 -0
  55. package/dist/core/self-evolution/scope-gate.d.ts +66 -0
  56. package/dist/core/self-evolution/scope-gate.js +107 -0
  57. package/dist/core/self-evolution/success-channel.js +2 -2
  58. package/dist/core/self-evolution/tamper-check.d.ts +24 -0
  59. package/dist/core/self-evolution/tamper-check.js +236 -0
  60. package/dist/core/self-evolution/tool-evolution.js +2 -13
  61. package/dist/core/self-evolution/verdict.d.ts +8 -5
  62. package/dist/core/self-evolution/verdict.js +4 -7
  63. package/dist/core/templates/workflows/gen-tests.js +1 -1
  64. package/dist/core/templates/workflows/learn.d.ts +3 -2
  65. package/dist/core/templates/workflows/learn.js +21 -18
  66. package/dist/core/templates/workflows/self-evolving.d.ts +6 -4
  67. package/dist/core/templates/workflows/self-evolving.js +62 -172
  68. package/dist/core/trajectory/scrub.d.ts +27 -0
  69. package/dist/core/trajectory/scrub.js +79 -0
  70. package/dist/core/trajectory/skeleton.d.ts +27 -1
  71. package/dist/core/trajectory/skeleton.js +152 -8
  72. package/dist/dashboard/data.d.ts +25 -51
  73. package/dist/dashboard/data.js +68 -180
  74. package/dist/dashboard/react-client.js +458 -503
  75. package/dist/dashboard/react-styles.js +3 -3
  76. package/dist/dashboard/server.js +23 -17
  77. package/dist/ui/ascii-patterns.d.ts +7 -15
  78. package/dist/ui/ascii-patterns.js +123 -54
  79. package/dist/ui/welcome-screen.d.ts +0 -14
  80. package/dist/ui/welcome-screen.js +16 -35
  81. package/package.json +1 -1
  82. package/dist/core/self-evolution/ga-selection.d.ts +0 -94
  83. package/dist/core/self-evolution/ga-selection.js +0 -153
  84. package/dist/core/self-evolution/proposer-agent.d.ts +0 -182
  85. package/dist/core/self-evolution/proposer-agent.js +0 -326
  86. package/dist/core/self-evolution/replay-runner.d.ts +0 -100
  87. package/dist/core/self-evolution/replay-runner.js +0 -170
  88. package/dist/core/self-evolution/replay.d.ts +0 -45
  89. package/dist/core/self-evolution/replay.js +0 -56
  90. package/dist/core/self-evolution/template-variants.d.ts +0 -62
  91. package/dist/core/self-evolution/template-variants.js +0 -171
  92. package/dist/core/self-evolution/trajectory.d.ts +0 -65
  93. package/dist/core/self-evolution/trajectory.js +0 -185
@@ -12,6 +12,5 @@ export declare class ArchiveCommand {
12
12
  private selectChange;
13
13
  private getArchiveDate;
14
14
  private checkArchiveReadiness;
15
- private recordSelfEvolutionObservations;
16
15
  }
17
16
  //# sourceMappingURL=archive.d.ts.map
@@ -5,8 +5,6 @@ import { getChangeReadiness, } from './change-readiness.js';
5
5
  import { Validator } from './validation/validator.js';
6
6
  import chalk from 'chalk';
7
7
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
8
- import { evaluateTaskDecompositionForChange, isEvolutionPartEnabled, parseEvolutionSwitchOptions, recordTemplateVariantObservation, verifySpecCodeAlignmentForChange, } from './self-evolution/index.js';
9
- import { isReadOnlyMode } from '../runtime/side-effects.js';
10
8
  /**
11
9
  * Recursively copy a directory. Used when fs.rename fails (e.g. EPERM on Windows).
12
10
  */
@@ -248,7 +246,6 @@ export class ArchiveCommand {
248
246
  }
249
247
  // Create archive directory if needed
250
248
  await fs.mkdir(archiveDir, { recursive: true });
251
- await this.recordSelfEvolutionObservations(targetPath, changeName, parseEvolutionSwitchOptions(options));
252
249
  // Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
253
250
  await moveDirectory(changeDir, archivePath);
254
251
  console.log(`Change '${changeName}' archived as '${archiveName}'.`);
@@ -338,60 +335,5 @@ export class ArchiveCommand {
338
335
  console.log(chalk.yellow('Archive cancelled. Complete the change or rerun with --force-incomplete.'));
339
336
  return false;
340
337
  }
341
- async recordSelfEvolutionObservations(projectRoot, changeName, evolutionSwitches) {
342
- if (isReadOnlyMode()) {
343
- return;
344
- }
345
- if (!isEvolutionPartEnabled(evolutionSwitches, 'template-variants')) {
346
- return;
347
- }
348
- try {
349
- const taskQuality = isEvolutionPartEnabled(evolutionSwitches, 'task-decomposition')
350
- ? evaluateTaskDecompositionForChange({ projectRoot, changeName })
351
- : null;
352
- const alignment = isEvolutionPartEnabled(evolutionSwitches, 'alignment-verifier')
353
- ? verifySpecCodeAlignmentForChange({ projectRoot, changeName })
354
- : null;
355
- const artifacts = ['proposal', 'usecases', 'specs', 'design', 'tasks'];
356
- let recorded = 0;
357
- for (const artifactId of artifacts) {
358
- if (recordTemplateVariantObservation({
359
- projectRoot,
360
- schemaName: 'spec-driven',
361
- artifactId,
362
- changeName,
363
- observation: {
364
- taskCompletionRatio: taskQuality?.metrics.completionRatio,
365
- taskQualityScore: taskQuality?.score,
366
- alignmentScore: alignment?.score,
367
- reworkCount: countReworkSignals((taskQuality?.findings.length ?? 0) + (alignment?.findings.length ?? 0)),
368
- notes: [
369
- 'archive observation',
370
- taskQuality ? `task=${taskQuality.score.toFixed(2)}` : 'task=disabled',
371
- alignment ? `alignment=${alignment.score.toFixed(2)}` : 'alignment=disabled',
372
- ].join(': '),
373
- },
374
- })) {
375
- recorded++;
376
- }
377
- }
378
- if (recorded > 0) {
379
- console.log(`Self-evolution observations recorded for ${recorded} template variant(s).`);
380
- }
381
- }
382
- catch (error) {
383
- const message = error instanceof Error ? error.message : String(error);
384
- console.log(chalk.yellow(`Warning: self-evolution observation skipped: ${message}`));
385
- }
386
- }
387
- }
388
- function countReworkSignals(findingCount) {
389
- if (findingCount <= 0)
390
- return 0;
391
- if (findingCount <= 2)
392
- return 1;
393
- if (findingCount <= 5)
394
- return 2;
395
- return 3;
396
338
  }
397
339
  //# sourceMappingURL=archive.js.map
@@ -1,6 +1,6 @@
1
1
  import { ArtifactGraph } from './graph.js';
2
2
  import type { CompletedSet } from './types.js';
3
- import { type EvolutionSwitchInput, type EvolutionSwitches, type TemplateVariantSelection } from '../self-evolution/index.js';
3
+ import { type EvolutionSwitchInput, type EvolutionSwitches } from '../self-evolution/index.js';
4
4
  /**
5
5
  * Error thrown when loading a template fails.
6
6
  */
@@ -49,9 +49,7 @@ export interface ArtifactInstructions {
49
49
  rules: string[] | undefined;
50
50
  /** Template content (structure to follow - this IS the output format) */
51
51
  template: string;
52
- /** Selected template variant, if project self-evolution chose one */
53
- templateVariant?: TemplateVariantSelection;
54
- /** Self-evolution context assembled from template selection, archive memory, and quality signals */
52
+ /** Self-evolution context assembled from archive memory and quality signals */
55
53
  selfEvolutionContext?: string;
56
54
  /** Per-run evolution switches that controlled this instruction payload */
57
55
  evolutionSwitches?: EvolutionSwitches;
@@ -7,7 +7,7 @@ import { resolveSchemaForChange } from '../../utils/change-metadata.js';
7
7
  import { readProjectConfig, validateConfigRules, validateConfigSelfEvolutionTargets, } from '../project-config.js';
8
8
  import { CANONICAL_TARGETS } from '../self-evolution/canonical-targets.js';
9
9
  import { ensureDesignConstitution } from '../design-constitution.js';
10
- import { findSimilarArchiveExperiences, isEvolutionPartEnabled, loadTemplateWithVariant, renderArchiveExperienceBlock, renderTaskStrategyContext, renderTemplateVariantContext, resolveEvolutionSwitches, evaluateTaskDecomposition, } from '../self-evolution/index.js';
10
+ import { findSimilarArchiveExperiences, isEvolutionPartEnabled, renderArchiveExperienceBlock, renderTaskStrategyContext, resolveEvolutionSwitches, evaluateTaskDecomposition, } from '../self-evolution/index.js';
11
11
  // Session-level cache for validation warnings (avoid repeating same warnings)
12
12
  const shownWarnings = new Set();
13
13
  /**
@@ -96,27 +96,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
96
96
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
97
97
  }
98
98
  const evolutionSwitches = resolveEvolutionSwitches(options.evolution);
99
- const templateWithVariant = isEvolutionPartEnabled(evolutionSwitches, 'template-variants')
100
- ? loadTemplateWithVariant({
101
- schemaName: context.schemaName,
102
- artifactId: artifact.id,
103
- templatePath: artifact.template,
104
- projectRoot: context.projectRoot,
105
- loadBuiltIn: loadTemplate,
106
- })
107
- : {
108
- content: loadTemplate(context.schemaName, artifact.template, context.projectRoot),
109
- selection: {
110
- id: 'built-in',
111
- schema: context.schemaName,
112
- artifact: artifact.id,
113
- templatePath: artifact.template,
114
- score: 0.5,
115
- observationCount: 0,
116
- source: 'built-in',
117
- reason: 'template evolution disabled for this run',
118
- },
119
- };
99
+ const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
120
100
  const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
121
101
  const unlocks = getUnlockedArtifacts(context.graph, artifactId);
122
102
  // Use projectRoot from context if not explicitly provided
@@ -167,7 +147,6 @@ export function generateInstructions(context, artifactId, projectRoot, options =
167
147
  const selfEvolutionContext = buildSelfEvolutionContext({
168
148
  context,
169
149
  artifactId,
170
- templateVariant: templateWithVariant.selection,
171
150
  configContext,
172
151
  evolutionSwitches,
173
152
  });
@@ -181,8 +160,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
181
160
  instruction: artifact.instruction,
182
161
  context: configContext,
183
162
  rules: configRules,
184
- template: templateWithVariant.content,
185
- templateVariant: templateWithVariant.selection,
163
+ template: templateContent,
186
164
  selfEvolutionContext,
187
165
  evolutionSwitches,
188
166
  dependencies,
@@ -191,12 +169,6 @@ export function generateInstructions(context, artifactId, projectRoot, options =
191
169
  }
192
170
  function buildSelfEvolutionContext(args) {
193
171
  const blocks = [];
194
- if (isEvolutionPartEnabled(args.evolutionSwitches, 'template-variants') && args.templateVariant) {
195
- const variantContext = renderTemplateVariantContext(args.templateVariant);
196
- if (variantContext) {
197
- blocks.push(variantContext);
198
- }
199
- }
200
172
  if (isEvolutionPartEnabled(args.evolutionSwitches, 'archive-memory')) {
201
173
  const query = [
202
174
  args.context.changeName,
@@ -6,10 +6,10 @@
6
6
  * healthPenalty = normalized SlopCodeBench code-health penalty
7
7
  * (structural_erosion ⊕ verbosity)
8
8
  *
9
- * Functional correctness is ALSO used as a hard GATE at GA selection/promotion
10
- * (a variant whose code fails its tests cannot win); this module only computes
11
- * the continuous loss scalar used to rank passers. See
12
- * todo/learn-self-evolution-migration-plan.md.
9
+ * This loss is the objective evidence the 奖励智能体 (REWARD AGENT) anchors
10
+ * on for an episode: functional correctness is ALSO a hard GATE
11
+ * (a change whose code fails its tests cannot be promoted), while this module
12
+ * only computes the continuous loss scalar used to compare passers.
13
13
  */
14
14
  export interface PerChangeLoss {
15
15
  /** 1 − pass_rate, in [0,1]. */
@@ -45,7 +45,7 @@ export interface ComputeLossInput {
45
45
  * the loss is byte-identical to the functional⊕health baseline regardless of
46
46
  * `verified` — the trajectory signal is recorded on the FitnessSample for
47
47
  * auditing without yet moving selection. Raise it to let unverified
48
- * candidates be down-weighted (never hard-disqualified) in GA selection.
48
+ * candidates be down-weighted (never hard-disqualified) when comparing them.
49
49
  */
50
50
  unverifiedWeight?: number;
51
51
  }
@@ -6,10 +6,10 @@
6
6
  * healthPenalty = normalized SlopCodeBench code-health penalty
7
7
  * (structural_erosion ⊕ verbosity)
8
8
  *
9
- * Functional correctness is ALSO used as a hard GATE at GA selection/promotion
10
- * (a variant whose code fails its tests cannot win); this module only computes
11
- * the continuous loss scalar used to rank passers. See
12
- * todo/learn-self-evolution-migration-plan.md.
9
+ * This loss is the objective evidence the 奖励智能体 (REWARD AGENT) anchors
10
+ * on for an episode: functional correctness is ALSO a hard GATE
11
+ * (a change whose code fails its tests cannot be promoted), while this module
12
+ * only computes the continuous loss scalar used to compare passers.
13
13
  */
14
14
  function clamp01(v) {
15
15
  if (Number.isNaN(v))
@@ -57,6 +57,14 @@ function findAssertion(lines, from) {
57
57
  function cleanToken(value) {
58
58
  return value.replace(/^[`'"]+|[`'"]+$/g, '');
59
59
  }
60
+ /**
61
+ * POSIX-normalize a path so a Windows pytest path (`tests\test_x.py`) matches the
62
+ * already-POSIX-normalized file-edit paths in the action skeleton — the reward
63
+ * agent's renamed/edited-test caveat compares the two by exact string.
64
+ */
65
+ function toPosix(p) {
66
+ return p.replace(/\\/g, '/');
67
+ }
60
68
  /**
61
69
  * Extract failing test ids + assertion lines from observed runner output.
62
70
  * Returns `[]` when nothing is recognized. Deduplicates by testId, preserves
@@ -86,7 +94,7 @@ export function parseTestFailures(output) {
86
94
  const inline = pytest[2]?.trim();
87
95
  push({
88
96
  testId,
89
- file: testId.split('::')[0],
97
+ file: toPosix(testId.split('::')[0]),
90
98
  ...(inline
91
99
  ? { assertion: capAssertion(inline) }
92
100
  : (() => {
@@ -98,7 +106,7 @@ export function parseTestFailures(output) {
98
106
  }
99
107
  const vitest = VITEST_FAIL_RE.exec(line);
100
108
  if (vitest) {
101
- const file = cleanToken(vitest[1]);
109
+ const file = toPosix(cleanToken(vitest[1]));
102
110
  const rest = vitest[2]?.trim();
103
111
  const testId = rest ? `${file} > ${rest}` : file;
104
112
  const assertion = findAssertion(lines, i);
@@ -25,6 +25,25 @@ export declare const ProjectConfigSchema: z.ZodObject<{
25
25
  evolve: z.ZodBoolean;
26
26
  }, z.core.$strip>>>;
27
27
  focus: z.ZodOptional<z.ZodBoolean>;
28
+ advantageRollbackThreshold: z.ZodOptional<z.ZodNumber>;
29
+ editBudget: z.ZodOptional<z.ZodNumber>;
30
+ reward: z.ZodOptional<z.ZodObject<{
31
+ samples: z.ZodOptional<z.ZodNumber>;
32
+ noiseFloor: z.ZodOptional<z.ZodNumber>;
33
+ orderSwap: z.ZodOptional<z.ZodBoolean>;
34
+ requireCorrectnessGate: z.ZodOptional<z.ZodBoolean>;
35
+ tamperCheck: z.ZodOptional<z.ZodEnum<{
36
+ off: "off";
37
+ flag: "flag";
38
+ block: "block";
39
+ }>>;
40
+ }, z.core.$strip>>;
41
+ critic: z.ZodOptional<z.ZodObject<{
42
+ baselineMode: z.ZodOptional<z.ZodEnum<{
43
+ "re-test": "re-test";
44
+ "re-do": "re-do";
45
+ }>>;
46
+ }, z.core.$strip>>;
28
47
  }, z.core.$strip>>;
29
48
  health: z.ZodOptional<z.ZodObject<{
30
49
  source: z.ZodDefault<z.ZodEnum<{
@@ -49,6 +49,53 @@ export const ProjectConfigSchema = z.object({
49
49
  // hint instead of silently dropping them. Set `focus: false` (or pass
50
50
  // `learn --no-focus`) to drop frozen-kind signals silently, as before.
51
51
  focus: z.boolean().optional(),
52
+ // Loop v2 (self-evolution as in-context RL): the advantage = reward(主臂) −
53
+ // reward(基线臂) threshold below which the episode-orchestrator rolls the
54
+ // 策略 POLICY back to the prior version before the 演进智能体 EVOLVING AGENT
55
+ // runs. Default 0 — a non-positive advantage (the new policy did not beat
56
+ // the baseline) triggers a rollback. Optional/omitted ⇒ the orchestrator's
57
+ // built-in default applies.
58
+ advantageRollbackThreshold: z.number().optional(),
59
+ // Loop v2: the edit budget L (max changed lines, added + removed) the
60
+ // 演进智能体 EVOLVING AGENT's ONE bounded edit may total. Default 40.
61
+ // Optional/omitted ⇒ the agent's DEFAULT_EVOLVING_AGENT_EDIT_BUDGET applies.
62
+ editBudget: z.number().optional(),
63
+ // Loop v2 — 奖励智能体 REWARD AGENT judge-quality knobs. ALL optional; omitted
64
+ // ⇒ the historical single-sample, flag-only behaviour (no extra LLM spawns).
65
+ reward: z
66
+ .object({
67
+ // ② How many judged duels per episode. Default 1 (single sample, no
68
+ // extra spawns). >1 enables the A/A noise floor + SPRT + order-swap.
69
+ samples: z.number().optional(),
70
+ // ② Minimum |advantage| to trust; within the floor ⇒ insufficient-signal.
71
+ // Omitted ⇒ measured from an A/A pair when samples>1, else unused.
72
+ noiseFloor: z.number().optional(),
73
+ // ③ Swap arm presentation order across samples to cancel position bias.
74
+ orderSwap: z.boolean().optional(),
75
+ // ① Enforce the correctness hard-gate inside the judge (default on).
76
+ requireCorrectnessGate: z.boolean().optional(),
77
+ // ④ Test-tamper handling: 'off' (no check), 'flag' (annotate only,
78
+ // default), or 'block' (force insufficient-signal + reject-buffer).
79
+ tamperCheck: z.enum(['off', 'flag', 'block']).optional(),
80
+ })
81
+ .optional(),
82
+ // Loop v2 — CRITIC AGENT(基线智能体 baseline agent)baseline construction.
83
+ // 're-do' (default): the baseline arm RE-DOES the change under the prior
84
+ // policy vN — it resets the change's GENERATED artifacts (design.md,
85
+ // tasks.md), re-authors design under the installed vN template, then
86
+ // re-implements → gen-test → run-test. So advantage = reward(主臂) −
87
+ // reward(基线臂) reflects the POLICY change, not re-run noise. Faithful
88
+ // when the change's implementation is still uncommitted at episode time
89
+ // (the workflow default) and an isolated git worktree can be created;
90
+ // on a non-git copy fallback it degrades to a re-measure (documented).
91
+ // 're-test': the prior behaviour — re-run the EXISTING change's tests
92
+ // under vN's template (cheaper, but an already-authored change does not
93
+ // exercise the design template). Omitted ⇒ 're-do'.
94
+ critic: z
95
+ .object({
96
+ baselineMode: z.enum(['re-test', 're-do']).optional(),
97
+ })
98
+ .optional(),
52
99
  })
53
100
  .optional()
54
101
  .describe('Per-canonical-target self-evolution toggles'),
@@ -218,6 +265,55 @@ export function readProjectConfig(projectRoot) {
218
265
  console.warn(`Invalid 'selfEvolution.focus' in config (must be boolean), ignoring`);
219
266
  }
220
267
  }
268
+ // Loop v2 numeric knobs. Resilient: a non-number is dropped with a
269
+ // warning (the orchestrator/agent default then applies); omitted ⇒
270
+ // undefined (byte-identical to configs that never set them).
271
+ const advThresholdResult = z.number().safeParse(rawSE.advantageRollbackThreshold);
272
+ if (advThresholdResult.success) {
273
+ selfEvolution.advantageRollbackThreshold = advThresholdResult.data;
274
+ }
275
+ else if (rawSE.advantageRollbackThreshold !== undefined) {
276
+ console.warn(`Invalid 'selfEvolution.advantageRollbackThreshold' in config (must be a number), ignoring`);
277
+ }
278
+ const editBudgetResult = z.number().safeParse(rawSE.editBudget);
279
+ if (editBudgetResult.success) {
280
+ selfEvolution.editBudget = editBudgetResult.data;
281
+ }
282
+ else if (rawSE.editBudget !== undefined) {
283
+ console.warn(`Invalid 'selfEvolution.editBudget' in config (must be a number), ignoring`);
284
+ }
285
+ // Loop v2 — 奖励智能体 REWARD AGENT knobs. Resilient: each sub-field is
286
+ // validated independently; a bad value is dropped with a warning (the
287
+ // judge/aggregator default applies). Omitted ⇒ undefined (single-sample,
288
+ // flag-only — byte-identical to configs that never set `reward`).
289
+ const rewardSchema = ProjectConfigSchema.shape.selfEvolution
290
+ .unwrap()
291
+ .shape.reward.unwrap();
292
+ const rewardResult = rewardSchema.safeParse(rawSE.reward);
293
+ if (rewardResult.success) {
294
+ if (Object.keys(rewardResult.data).length > 0) {
295
+ selfEvolution.reward = rewardResult.data;
296
+ }
297
+ }
298
+ else if (rawSE.reward !== undefined) {
299
+ console.warn(`Invalid 'selfEvolution.reward' in config (samples/noiseFloor numbers, ` +
300
+ `orderSwap/requireCorrectnessGate booleans, tamperCheck off|flag|block), ignoring`);
301
+ }
302
+ // Loop v2 — CRITIC AGENT knobs. Resilient: a bad value is dropped with a
303
+ // warning (the critic default 're-do' then applies). Omitted ⇒ undefined
304
+ // (byte-identical to configs that never set `critic`).
305
+ const criticSchema = ProjectConfigSchema.shape.selfEvolution
306
+ .unwrap()
307
+ .shape.critic.unwrap();
308
+ const criticResult = criticSchema.safeParse(rawSE.critic);
309
+ if (criticResult.success) {
310
+ if (Object.keys(criticResult.data).length > 0) {
311
+ selfEvolution.critic = criticResult.data;
312
+ }
313
+ }
314
+ else if (rawSE.critic !== undefined) {
315
+ console.warn(`Invalid 'selfEvolution.critic' in config (baselineMode must be 're-test' or 're-do'), ignoring`);
316
+ }
221
317
  config.selfEvolution = selfEvolution;
222
318
  }
223
319
  else {
@@ -1,4 +1,4 @@
1
- import type { CandidateRepoLayout } from './candidates.js';
1
+ import { type CandidateRepoLayout } from './candidates.js';
2
2
  export declare const FITNESS_RECORD_FILE = "fitness-record.jsonl";
3
3
  export interface CandidateFitnessRecord {
4
4
  /** ISO-8601 UTC timestamp the sample was recorded. */
@@ -44,4 +44,26 @@ export declare function isValidRecord(value: unknown): value is CandidateFitness
44
44
  * absent. Malformed/blank lines are skipped (forward-compatible).
45
45
  */
46
46
  export declare function readCandidateFitness(layout: CandidateRepoLayout, candidateId: string): Promise<AccumulatedFitness>;
47
+ /**
48
+ * The accumulated baseline reading for a target's de-facto canonical variant:
49
+ * the promoted candidate whose fitness sidecar supplies the baseline loss.
50
+ */
51
+ export interface PromotedBaseline {
52
+ candidateId: string;
53
+ meanLoss: number;
54
+ /** createdAt of the baseline candidate (for reporting). */
55
+ recordedAt: string;
56
+ }
57
+ /**
58
+ * The de-facto canonical baseline loss for a target: the accumulated mean loss
59
+ * of the MOST RECENTLY promoted candidate that has measured fitness. Returns
60
+ * null when no promoted candidate for the target has a non-null meanLoss (⇒
61
+ * downstream gates cannot use a baseline). Rolled-back candidates are excluded
62
+ * automatically (they are status 'rolled-back', not 'promoted').
63
+ *
64
+ * `listCandidates` already returns promoted candidates sorted by `createdAt`
65
+ * DESCENDING (ties by id), so the first proven candidate in iteration order is
66
+ * the most recently promoted one with fitness.
67
+ */
68
+ export declare function readPromotedBaselineLoss(layout: CandidateRepoLayout, targetId: string): Promise<PromotedBaseline | null>;
47
69
  //# sourceMappingURL=candidate-fitness.d.ts.map
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Per-candidate accumulated fitness (the GA outer loop's "weight history").
2
+ * Per-candidate accumulated fitness for the manual promote / evolve-from-edits
3
+ * channel (a candidate's accumulated outcome history).
3
4
  *
4
5
  * Each real change a candidate template-variant was active for appends one line
5
6
  * to an append-only `fitness-record.jsonl` sidecar in the candidate's directory
6
- * (`<baseDir>/<candidateId>/fitness-record.jsonl`). The genetic algorithm reads
7
- * the accumulated record to compare/select among competing variants and to feed
8
- * a data-driven promotion verdict. See
9
- * todo/learn-self-evolution-migration-plan.md.
7
+ * (`<baseDir>/<candidateId>/fitness-record.jsonl`). The manual promote /
8
+ * evolve-from-edits path reads the accumulated record to feed a data-driven
9
+ * promotion verdict. See todo/learn-self-evolution-migration-plan.md.
10
10
  *
11
11
  * Sidecar (not a candidate.json field) on purpose: candidate.json is the stable
12
12
  * proposal-time header; fitness is retrospective outcome data that grows over
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { promises as fs } from 'node:fs';
17
17
  import * as path from 'node:path';
18
+ import { listCandidates } from './candidates.js';
18
19
  export const FITNESS_RECORD_FILE = 'fitness-record.jsonl';
19
20
  function candidateDir(layout, candidateId) {
20
21
  return path.join(layout.baseDir, candidateId);
@@ -104,4 +105,29 @@ export async function readCandidateFitness(layout, candidateId) {
104
105
  recentTrend: trendOf(records.map((r) => r.loss)),
105
106
  };
106
107
  }
108
+ /**
109
+ * The de-facto canonical baseline loss for a target: the accumulated mean loss
110
+ * of the MOST RECENTLY promoted candidate that has measured fitness. Returns
111
+ * null when no promoted candidate for the target has a non-null meanLoss (⇒
112
+ * downstream gates cannot use a baseline). Rolled-back candidates are excluded
113
+ * automatically (they are status 'rolled-back', not 'promoted').
114
+ *
115
+ * `listCandidates` already returns promoted candidates sorted by `createdAt`
116
+ * DESCENDING (ties by id), so the first proven candidate in iteration order is
117
+ * the most recently promoted one with fitness.
118
+ */
119
+ export async function readPromotedBaselineLoss(layout, targetId) {
120
+ const promoted = await listCandidates(layout, { status: 'promoted', targetId });
121
+ for (const candidate of promoted) {
122
+ const fitness = await readCandidateFitness(layout, candidate.id);
123
+ if (fitness.meanLoss !== null) {
124
+ return {
125
+ candidateId: candidate.id,
126
+ meanLoss: fitness.meanLoss,
127
+ recordedAt: candidate.createdAt,
128
+ };
129
+ }
130
+ }
131
+ return null;
132
+ }
107
133
  //# sourceMappingURL=candidate-fitness.js.map
@@ -96,15 +96,6 @@ export interface CanonicalCandidate {
96
96
  evalReportPath?: string;
97
97
  /** Set by promotion once a decision has been recorded. Relative to candidate dir. */
98
98
  promotionDecisionPath?: string;
99
- /**
100
- * Population-based generation (`propose-canonical --variants N`): the shared id
101
- * of the sibling-variant cohort drafted from the SAME hint group in one run.
102
- * Absent for single-candidate (default) proposals. Lets the GA outer loop mark
103
- * siblings that lost the ranking as `outcompeted`.
104
- */
105
- variantGroup?: string;
106
- /** The improvement angle this variant was asked to pursue (population-based). */
107
- variantAngle?: string;
108
99
  }
109
100
  /**
110
101
  * A single full-file-replacement edit produced by the proposer agent: the