synergyspec-selfevolving 1.3.0 → 2.0.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 (113) hide show
  1. package/README.md +50 -19
  2. package/dist/commands/learn.d.ts +12 -1
  3. package/dist/commands/learn.js +373 -31
  4. package/dist/commands/self-evolution-episode.d.ts +177 -0
  5. package/dist/commands/self-evolution-episode.js +423 -0
  6. package/dist/commands/self-evolution.d.ts +12 -190
  7. package/dist/commands/self-evolution.js +179 -786
  8. package/dist/commands/workflow/status.js +3 -1
  9. package/dist/core/archive.d.ts +0 -1
  10. package/dist/core/archive.js +0 -58
  11. package/dist/core/artifact-graph/instruction-loader.d.ts +2 -4
  12. package/dist/core/artifact-graph/instruction-loader.js +3 -31
  13. package/dist/core/config-prompts.js +4 -0
  14. package/dist/core/fitness/health/health-metrics.d.ts +26 -56
  15. package/dist/core/fitness/health/health-metrics.js +19 -58
  16. package/dist/core/fitness/health/index.d.ts +15 -2
  17. package/dist/core/fitness/health/index.js +25 -1
  18. package/dist/core/fitness/health/local-source.d.ts +43 -4
  19. package/dist/core/fitness/health/local-source.js +181 -25
  20. package/dist/core/fitness/health/metric-source.d.ts +48 -19
  21. package/dist/core/fitness/health/metric-source.js +8 -18
  22. package/dist/core/fitness/health/resolve-source.js +4 -1
  23. package/dist/core/fitness/loss.d.ts +7 -7
  24. package/dist/core/fitness/loss.js +6 -6
  25. package/dist/core/fitness/sample.d.ts +10 -0
  26. package/dist/core/fitness/test-failures.d.ts +30 -0
  27. package/dist/core/fitness/test-failures.js +123 -0
  28. package/dist/core/learn/credit-path.d.ts +36 -0
  29. package/dist/core/learn/credit-path.js +198 -0
  30. package/dist/core/learn/trajectory-discovery.d.ts +39 -0
  31. package/dist/core/learn/trajectory-discovery.js +140 -0
  32. package/dist/core/learn.d.ts +39 -5
  33. package/dist/core/learn.js +131 -14
  34. package/dist/core/project-config.d.ts +4 -0
  35. package/dist/core/project-config.js +52 -1
  36. package/dist/core/self-evolution/candidate-fitness.d.ts +23 -1
  37. package/dist/core/self-evolution/candidate-fitness.js +31 -5
  38. package/dist/core/self-evolution/candidates.d.ts +0 -9
  39. package/dist/core/self-evolution/canonical-targets.d.ts +8 -4
  40. package/dist/core/self-evolution/canonical-targets.js +8 -4
  41. package/dist/core/self-evolution/critic-agent.d.ts +150 -0
  42. package/dist/core/self-evolution/critic-agent.js +487 -0
  43. package/dist/core/self-evolution/edits-contract.d.ts +53 -0
  44. package/dist/core/self-evolution/edits-contract.js +89 -0
  45. package/dist/core/self-evolution/episode-orchestrator.d.ts +197 -0
  46. package/dist/core/self-evolution/episode-orchestrator.js +534 -0
  47. package/dist/core/self-evolution/episode-store.d.ts +266 -0
  48. package/dist/core/self-evolution/episode-store.js +573 -0
  49. package/dist/core/self-evolution/evolution-switches.d.ts +1 -1
  50. package/dist/core/self-evolution/evolution-switches.js +5 -10
  51. package/dist/core/self-evolution/evolving-agent.d.ts +162 -0
  52. package/dist/core/self-evolution/evolving-agent.js +449 -0
  53. package/dist/core/self-evolution/health-baseline.d.ts +25 -6
  54. package/dist/core/self-evolution/health-baseline.js +30 -6
  55. package/dist/core/self-evolution/host-harness.d.ts +1 -2
  56. package/dist/core/self-evolution/host-harness.js +1 -2
  57. package/dist/core/self-evolution/index.d.ts +10 -6
  58. package/dist/core/self-evolution/index.js +19 -6
  59. package/dist/core/self-evolution/learn-hints.d.ts +31 -0
  60. package/dist/core/self-evolution/learn-hints.js +16 -0
  61. package/dist/core/self-evolution/learn-observation-adapter.d.ts +35 -0
  62. package/dist/core/self-evolution/learn-observation-adapter.js +285 -10
  63. package/dist/core/self-evolution/line-diff.d.ts +60 -0
  64. package/dist/core/self-evolution/line-diff.js +130 -0
  65. package/dist/core/self-evolution/policy/fs-safe.d.ts +19 -0
  66. package/dist/core/self-evolution/policy/fs-safe.js +89 -0
  67. package/dist/core/self-evolution/policy/index.d.ts +13 -0
  68. package/dist/core/self-evolution/policy/index.js +13 -0
  69. package/dist/core/self-evolution/policy/policy-store.d.ts +217 -0
  70. package/dist/core/self-evolution/policy/policy-store.js +774 -0
  71. package/dist/core/self-evolution/policy/reject-buffer.d.ts +48 -0
  72. package/dist/core/self-evolution/policy/reject-buffer.js +168 -0
  73. package/dist/core/self-evolution/promote.d.ts +1 -1
  74. package/dist/core/self-evolution/promote.js +6 -33
  75. package/dist/core/self-evolution/promotion.js +1 -2
  76. package/dist/core/self-evolution/proposer-agent.d.ts +41 -0
  77. package/dist/core/self-evolution/proposer-agent.js +94 -13
  78. package/dist/core/self-evolution/proposer-slice.d.ts +26 -0
  79. package/dist/core/self-evolution/proposer-slice.js +54 -0
  80. package/dist/core/self-evolution/reward-agent.d.ts +234 -0
  81. package/dist/core/self-evolution/reward-agent.js +564 -0
  82. package/dist/core/self-evolution/scope-gate.d.ts +66 -0
  83. package/dist/core/self-evolution/scope-gate.js +107 -0
  84. package/dist/core/self-evolution/success-channel.d.ts +79 -0
  85. package/dist/core/self-evolution/success-channel.js +361 -0
  86. package/dist/core/self-evolution/target-evolution.d.ts +11 -0
  87. package/dist/core/self-evolution/target-evolution.js +2 -0
  88. package/dist/core/self-evolution/tool-evolution.js +2 -13
  89. package/dist/core/self-evolution/verdict.d.ts +8 -5
  90. package/dist/core/self-evolution/verdict.js +4 -7
  91. package/dist/core/templates/skill-templates.d.ts +1 -0
  92. package/dist/core/templates/skill-templates.js +1 -0
  93. package/dist/core/templates/workflow-manifest.js +2 -0
  94. package/dist/core/templates/workflows/learn.d.ts +4 -2
  95. package/dist/core/templates/workflows/learn.js +25 -166
  96. package/dist/core/templates/workflows/self-evolving.d.ts +13 -0
  97. package/dist/core/templates/workflows/self-evolving.js +127 -0
  98. package/dist/core/trajectory/facts.d.ts +16 -0
  99. package/dist/core/trajectory/facts.js +12 -4
  100. package/dist/core/trajectory/skeleton.d.ts +43 -0
  101. package/dist/core/trajectory/skeleton.js +239 -0
  102. package/dist/dashboard/data.d.ts +25 -51
  103. package/dist/dashboard/data.js +68 -180
  104. package/dist/dashboard/react-client.js +458 -503
  105. package/dist/dashboard/react-styles.js +3 -3
  106. package/dist/dashboard/server.js +23 -17
  107. package/dist/ui/ascii-patterns.d.ts +7 -15
  108. package/dist/ui/ascii-patterns.js +123 -54
  109. package/dist/ui/welcome-screen.d.ts +0 -14
  110. package/dist/ui/welcome-screen.js +16 -35
  111. package/package.json +3 -1
  112. package/scripts/code-health.py +1066 -638
  113. package/scripts/slop_rules.yaml +2151 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 范围⊆诊断 scope⊆diagnosis gate for the 演进智能体 EVOLVING AGENT — loop v2
3
+ * (self-evolution as in-context RL).
4
+ *
5
+ * The 奖励智能体 REWARD AGENT names a set of GAPS, each anchored to a (file,
6
+ * section) the 文本梯度 textual gradient points at. The EVOLVING AGENT's ONE
7
+ * bounded edit must stay INSIDE those named sections — it may not wander off
8
+ * and rewrite an unrelated heading just because the file is editable. This gate
9
+ * is the check: from the line diff, compute each changed range's ENCLOSING
10
+ * section, and PASS iff every (file, section) the edit touches is covered by
11
+ * some diagnosis gap.
12
+ *
13
+ * Section addressing:
14
+ * - `.md` files: the nearest PRECEDING markdown heading of any `#`-level
15
+ * (`# …`, `## …`, …). A change before the first heading has section `''`
16
+ * (the file preamble).
17
+ * - YAML / other files: the nearest preceding TOP-LEVEL key (`key:` at column
18
+ * 0). A change before the first top-level key has section `''`.
19
+ *
20
+ * Coverage:
21
+ * - a gap `{file: '*'}` covers ANY file;
22
+ * - a gap `{section: '*'}` covers the WHOLE file;
23
+ * - otherwise the gap's `file` AND `section` must match exactly.
24
+ *
25
+ * Pure + dependency-free (golden-testable). Re-uses {@link lineDiff} so its
26
+ * notion of "changed lines" is identical to the ≤ L budget check.
27
+ */
28
+ import { lineDiff } from './line-diff.js';
29
+ function toPosix(p) {
30
+ return p.replace(/\\/g, '/');
31
+ }
32
+ /**
33
+ * The enclosing section of 1-based `line` in `proposedContent`. For `.md`
34
+ * files it is the nearest preceding heading's text; otherwise the nearest
35
+ * preceding top-level (`key:` at column 0) key. `''` when nothing precedes it.
36
+ */
37
+ function enclosingSection(relPath, proposedLines, line) {
38
+ const isMarkdown = /\.md$/i.test(relPath);
39
+ let section = '';
40
+ // Scan from the file top down to (and including) the changed line; the last
41
+ // matching anchor at-or-before it is the enclosing section.
42
+ const upto = Math.min(line, proposedLines.length);
43
+ for (let idx = 0; idx < upto; idx++) {
44
+ const text = proposedLines[idx];
45
+ if (isMarkdown) {
46
+ const h = /^#{1,6}\s+(.+?)\s*$/.exec(text);
47
+ if (h)
48
+ section = h[1];
49
+ }
50
+ else {
51
+ // Top-level key: an identifier-ish key at column 0 followed by ':'.
52
+ const k = /^([^\s:#][^:]*):(?:\s|$)/.exec(text);
53
+ if (k)
54
+ section = k[1].trim();
55
+ }
56
+ }
57
+ return section;
58
+ }
59
+ /** True iff some gap covers `(file, section)` per the wildcard rules. */
60
+ function isCovered(file, section, gaps) {
61
+ for (const gap of gaps) {
62
+ const fileOk = gap.file === '*' || toPosix(gap.file) === file;
63
+ if (!fileOk)
64
+ continue;
65
+ if (gap.section === '*' || gap.section === section)
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * Run the 范围⊆诊断 scope⊆diagnosis gate. For every edit, diff it against its
72
+ * current content, resolve each changed range's enclosing section, and flag any
73
+ * (file, section) not covered by a gap. Returns `{pass, violations}`; a pure
74
+ * deletion (no inserted range) is in-scope by construction (it removes named or
75
+ * unnamed lines but introduces no new out-of-scope section) and is not flagged.
76
+ */
77
+ export function checkScopeWithinDiagnosis(input) {
78
+ const currentByPath = new Map(input.currentFiles.map((f) => [toPosix(f.relPath), f.content]));
79
+ // Accumulate violations keyed by file+section so the lines coalesce per spot.
80
+ const violationByKey = new Map();
81
+ for (const edit of input.edits) {
82
+ const rel = toPosix(edit.relPath);
83
+ const current = currentByPath.get(rel) ?? '';
84
+ const proposedLines = edit.content.length === 0 ? [] : edit.content.replace(/\n$/, '').split('\n');
85
+ const d = lineDiff(current, edit.content);
86
+ for (const range of d.changedRanges) {
87
+ const section = enclosingSection(rel, proposedLines, range.startLine);
88
+ if (isCovered(rel, section, input.gaps))
89
+ continue;
90
+ const key = `${rel}${section}`;
91
+ const existing = violationByKey.get(key);
92
+ if (existing) {
93
+ existing.lines.push({ startLine: range.startLine, endLine: range.endLine });
94
+ }
95
+ else {
96
+ violationByKey.set(key, {
97
+ file: rel,
98
+ section,
99
+ lines: [{ startLine: range.startLine, endLine: range.endLine }],
100
+ });
101
+ }
102
+ }
103
+ }
104
+ const violations = [...violationByKey.values()];
105
+ return { pass: violations.length === 0, violations };
106
+ }
107
+ //# sourceMappingURL=scope-gate.js.map
@@ -0,0 +1,79 @@
1
+ import type { LearnReport } from '../learn.js';
2
+ export declare const PROTECTIONS_FILE = "protections.json";
3
+ /** One protected surface: a section of a canonical target implicated in green runs. */
4
+ export interface TargetProtection {
5
+ /** Producing canonical target id (e.g. `artifact-template:design`). */
6
+ targetId: string;
7
+ /** Design-section heading the credit path resolved, or `'*'` for the whole target. */
8
+ section: string;
9
+ /** Unique change names whose verified-green runs implicated this section. */
10
+ sourceChanges: string[];
11
+ /** Count of verified-green runs (mining calls) that implicated this section. */
12
+ occurrences: number;
13
+ /** ISO-8601 timestamp of the most recent implicating run. */
14
+ lastObservedAt: string;
15
+ }
16
+ export interface MineSuccessSignalsResult {
17
+ /** True iff the report was verified-GREEN and the channel ran. */
18
+ mined: boolean;
19
+ /** Protections recorded/incremented this call (0 when not green or persist failed). */
20
+ protectionsWritten: number;
21
+ /** Absolute paths of exemplar files written this call. */
22
+ exemplarsWritten: string[];
23
+ /** Target ids that received a protection this call (for one-line summaries). */
24
+ protectedTargets: string[];
25
+ }
26
+ /**
27
+ * The success-channel "green" predicate. DELIBERATELY the same predicate shape
28
+ * as the observed-verified gate conditions in `promote.ts` `isEvidenceComplete`
29
+ * (trajectoryFacts.verified === true AND observedStatus === 'success' OR
30
+ * observedPassRate >= 1) — read that function first when changing this one, so
31
+ * success-mining and the gate can never disagree about what "green" means.
32
+ */
33
+ export declare function isVerifiedGreen(report: LearnReport): boolean;
34
+ /**
35
+ * Read the persisted protections for one canonical target. Defensive read —
36
+ * absent/corrupt file means "no protections yet", never an error.
37
+ */
38
+ export declare function readProtections(projectRoot: string, targetId: string): Promise<TargetProtection[]>;
39
+ /**
40
+ * Exemplar file paths for a target, most-recent first (mtime desc, then
41
+ * filename desc as the deterministic tiebreak — the same order the retention
42
+ * cap uses). `[]` when the dir is absent.
43
+ */
44
+ export declare function listExemplarFiles(projectRoot: string, targetId: string): Promise<string[]>;
45
+ /**
46
+ * Parse the ✅-status rows of the spec-tests.md Requirement Traceability
47
+ * Matrix and return their Test Case files. Only rows INSIDE the
48
+ * `## Requirement Traceability Matrix` section count (the PBT Coverage table
49
+ * shares the pipe/✅ shape but its test column is scenario-level). The status
50
+ * cell is located by content (contains '✅') and the Test Case cell is the one
51
+ * just before it, so benign column drift does not blank the channel.
52
+ */
53
+ export declare function passingTestFilesFromMatrix(specTestsMd: string): string[];
54
+ /**
55
+ * Render the bounded DO-NOT-PRUNE block: one line per protected section with
56
+ * its passing-run count, plus the exemplar file paths. Empty string when there
57
+ * is nothing to protect. Deterministic: sorted by occurrences desc, then
58
+ * targetId/section.
59
+ */
60
+ export declare function renderDoNotPruneBlock(protections: TargetProtection[], exemplarPaths: string[]): string;
61
+ /**
62
+ * Mine a verified-GREEN learn report for protections + exemplars.
63
+ *
64
+ * NOT green ⇒ `{ mined: false }` and NOTHING is written — the success channel
65
+ * must never manufacture signal from an unverified or failing run (the exact
66
+ * inverse discipline of the observed-verified promote gate).
67
+ *
68
+ * Green ⇒ walk each ✅ matrix row's test back through the credit path
69
+ * (test → UC → task → design section), protect the producing target's section,
70
+ * persist merged protections, and write bounded real-excerpt exemplars
71
+ * (3 most-recent kept per target).
72
+ */
73
+ export declare function mineSuccessSignals(opts: {
74
+ projectRoot: string;
75
+ report: LearnReport;
76
+ /** Injectable clock for tests; defaults to the wall clock. */
77
+ now?: () => Date;
78
+ }): Promise<MineSuccessSignalsResult>;
79
+ //# sourceMappingURL=success-channel.d.ts.map
@@ -0,0 +1,361 @@
1
+ /**
2
+ * SUCCESS CHANNEL (R4: "the fence is the feature").
3
+ *
4
+ * The failure path mines red runs for corrective evolution hints; this module
5
+ * is its mirror: a verified-GREEN run is mined for (a) load-bearing
6
+ * PROTECTIONS — the design sections / tasks the passing tests' credit paths
7
+ * implicate as producers of working behavior — and (b) EXEMPLARS — bounded
8
+ * REAL excerpts of those artifacts. Both flow into the proposer surfaces as
9
+ * DO-NOT-PRUNE constraints, so an evolution candidate can never "improve" a
10
+ * template by hollowing out the parts that demonstrably produce green runs.
11
+ *
12
+ * Crucially this is a SIDE-WRITE channel only: a clean run still NEVER authors
13
+ * or triggers a corrective candidate (abstain-on-success is preserved — the
14
+ * channel writes protections/exemplars, never anything under `candidates/`).
15
+ *
16
+ * Persistence (best-effort, mirroring `health-baseline.ts`): a missing/corrupt
17
+ * protections file reads as empty, and a failed write never fails the learn
18
+ * run that triggered the mining.
19
+ */
20
+ import { promises as fs } from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { walkCreditPath } from '../learn/credit-path.js';
23
+ import { producingTargetIdForCreditPath } from './learn-observation-adapter.js';
24
+ export const PROTECTIONS_FILE = 'protections.json';
25
+ /**
26
+ * The success-channel "green" predicate. DELIBERATELY the same predicate shape
27
+ * as the observed-verified gate conditions in `promote.ts` `isEvidenceComplete`
28
+ * (trajectoryFacts.verified === true AND observedStatus === 'success' OR
29
+ * observedPassRate >= 1) — read that function first when changing this one, so
30
+ * success-mining and the gate can never disagree about what "green" means.
31
+ */
32
+ export function isVerifiedGreen(report) {
33
+ const facts = report.fitnessSample?.trajectoryFacts;
34
+ if (!facts || facts.verified !== true)
35
+ return false;
36
+ const greenByStatus = facts.observedStatus === 'success';
37
+ const greenByPassRate = typeof facts.observedPassRate === 'number' && facts.observedPassRate >= 1;
38
+ return greenByStatus || greenByPassRate;
39
+ }
40
+ function selfEvolutionDir(projectRoot) {
41
+ return path.join(path.resolve(projectRoot), '.synergyspec-selfevolving', 'self-evolution');
42
+ }
43
+ function protectionsPath(projectRoot) {
44
+ return path.join(selfEvolutionDir(projectRoot), PROTECTIONS_FILE);
45
+ }
46
+ /** Exemplar dir for a target — `:` is not portable in dir names, so `__`. */
47
+ function exemplarDir(projectRoot, targetId) {
48
+ return path.join(selfEvolutionDir(projectRoot), 'exemplars', targetId.replace(/:/g, '__'));
49
+ }
50
+ async function readFileOrUndefined(abs) {
51
+ try {
52
+ return await fs.readFile(abs, 'utf8');
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ function isValidProtection(value) {
59
+ if (!value || typeof value !== 'object')
60
+ return false;
61
+ const p = value;
62
+ return (typeof p.targetId === 'string' &&
63
+ typeof p.section === 'string' &&
64
+ Array.isArray(p.sourceChanges) &&
65
+ p.sourceChanges.every((c) => typeof c === 'string') &&
66
+ typeof p.occurrences === 'number' &&
67
+ Number.isFinite(p.occurrences) &&
68
+ typeof p.lastObservedAt === 'string');
69
+ }
70
+ /** Read ALL persisted protections; `[]` when the file is absent/unparseable. */
71
+ async function readAllProtections(projectRoot) {
72
+ const raw = await readFileOrUndefined(protectionsPath(projectRoot));
73
+ if (raw === undefined)
74
+ return [];
75
+ let parsed;
76
+ try {
77
+ parsed = JSON.parse(raw);
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ if (!parsed || typeof parsed !== 'object')
83
+ return [];
84
+ const list = parsed.protections;
85
+ if (!Array.isArray(list))
86
+ return [];
87
+ return list.filter(isValidProtection);
88
+ }
89
+ /**
90
+ * Read the persisted protections for one canonical target. Defensive read —
91
+ * absent/corrupt file means "no protections yet", never an error.
92
+ */
93
+ export async function readProtections(projectRoot, targetId) {
94
+ return (await readAllProtections(projectRoot)).filter((p) => p.targetId === targetId);
95
+ }
96
+ /**
97
+ * Exemplar file paths for a target, most-recent first (mtime desc, then
98
+ * filename desc as the deterministic tiebreak — the same order the retention
99
+ * cap uses). `[]` when the dir is absent.
100
+ */
101
+ export async function listExemplarFiles(projectRoot, targetId) {
102
+ const dir = exemplarDir(projectRoot, targetId);
103
+ let names;
104
+ try {
105
+ names = (await fs.readdir(dir)).filter((n) => n.endsWith('.md'));
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ const stats = [];
111
+ for (const name of names) {
112
+ const abs = path.join(dir, name);
113
+ try {
114
+ const st = await fs.stat(abs);
115
+ stats.push({ abs, name, mtimeMs: st.mtimeMs });
116
+ }
117
+ catch {
118
+ // raced deletion — skip
119
+ }
120
+ }
121
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs || b.name.localeCompare(a.name));
122
+ return stats.map((s) => s.abs);
123
+ }
124
+ /** Keep only the `keep` most-recent exemplar files in a target dir. */
125
+ const EXEMPLARS_KEPT_PER_TARGET = 3;
126
+ /** Per-excerpt caps for the exemplar substrate (real text, bounded). */
127
+ const DESIGN_EXCERPT_CAP = 1500;
128
+ const TASK_EXCERPT_CAP = 500;
129
+ function capText(text, max) {
130
+ const t = text.trim();
131
+ return t.length > max ? `${t.slice(0, max - 1)}…` : t;
132
+ }
133
+ /**
134
+ * Parse the ✅-status rows of the spec-tests.md Requirement Traceability
135
+ * Matrix and return their Test Case files. Only rows INSIDE the
136
+ * `## Requirement Traceability Matrix` section count (the PBT Coverage table
137
+ * shares the pipe/✅ shape but its test column is scenario-level). The status
138
+ * cell is located by content (contains '✅') and the Test Case cell is the one
139
+ * just before it, so benign column drift does not blank the channel.
140
+ */
141
+ export function passingTestFilesFromMatrix(specTestsMd) {
142
+ const lines = specTestsMd.split(/\r?\n/);
143
+ const files = [];
144
+ const seen = new Set();
145
+ let inMatrix = false;
146
+ for (const line of lines) {
147
+ const heading = line.match(/^(#{2,3})\s+(.+)$/);
148
+ if (heading) {
149
+ inMatrix = /requirement traceability matrix/i.test(heading[2]);
150
+ continue;
151
+ }
152
+ if (!inMatrix || !line.trimStart().startsWith('|'))
153
+ continue;
154
+ const cells = line.split('|').map((c) => c.trim());
155
+ const statusIdx = cells.findIndex((c) => c.includes('✅'));
156
+ if (statusIdx <= 0)
157
+ continue;
158
+ const testCell = cells[statusIdx - 1] ?? '';
159
+ for (const token of testCell.split(',')) {
160
+ // Strip backticks and a trailing `:line` suffix; keep only path-shaped tokens.
161
+ const file = token.replace(/`/g, '').trim().replace(/:\d+$/, '');
162
+ if (file.length === 0 || !/[\\/.]/.test(file))
163
+ continue;
164
+ if (seen.has(file))
165
+ continue;
166
+ seen.add(file);
167
+ files.push(file);
168
+ }
169
+ }
170
+ return files;
171
+ }
172
+ /** Render cap for the DO-NOT-PRUNE block fed into prompts / CLI output. */
173
+ const DO_NOT_PRUNE_BLOCK_CAP = 1200;
174
+ /** Short human label for a target id inside the rendered block. */
175
+ function targetLabel(targetId) {
176
+ return targetId.startsWith('artifact-template:')
177
+ ? targetId.slice('artifact-template:'.length)
178
+ : targetId;
179
+ }
180
+ /**
181
+ * Render the bounded DO-NOT-PRUNE block: one line per protected section with
182
+ * its passing-run count, plus the exemplar file paths. Empty string when there
183
+ * is nothing to protect. Deterministic: sorted by occurrences desc, then
184
+ * targetId/section.
185
+ */
186
+ export function renderDoNotPruneBlock(protections, exemplarPaths) {
187
+ if (protections.length === 0)
188
+ return '';
189
+ const sorted = [...protections].sort((a, b) => b.occurrences - a.occurrences ||
190
+ a.targetId.localeCompare(b.targetId) ||
191
+ a.section.localeCompare(b.section));
192
+ const lines = [];
193
+ for (const p of sorted) {
194
+ const where = p.section === '*' ? `${targetLabel(p.targetId)} (whole target)` : `${targetLabel(p.targetId)} §"${p.section}"`;
195
+ lines.push(`- ${where} — implicated in ${p.occurrences} passing run(s); do not delete or hollow out`);
196
+ }
197
+ if (exemplarPaths.length > 0) {
198
+ lines.push(`exemplars: ${exemplarPaths.join(', ')}`);
199
+ }
200
+ const block = lines.join('\n');
201
+ return block.length > DO_NOT_PRUNE_BLOCK_CAP
202
+ ? `${block.slice(0, DO_NOT_PRUNE_BLOCK_CAP - 1)}…`
203
+ : block;
204
+ }
205
+ /**
206
+ * Mine a verified-GREEN learn report for protections + exemplars.
207
+ *
208
+ * NOT green ⇒ `{ mined: false }` and NOTHING is written — the success channel
209
+ * must never manufacture signal from an unverified or failing run (the exact
210
+ * inverse discipline of the observed-verified promote gate).
211
+ *
212
+ * Green ⇒ walk each ✅ matrix row's test back through the credit path
213
+ * (test → UC → task → design section), protect the producing target's section,
214
+ * persist merged protections, and write bounded real-excerpt exemplars
215
+ * (3 most-recent kept per target).
216
+ */
217
+ export async function mineSuccessSignals(opts) {
218
+ const { projectRoot, report } = opts;
219
+ if (!isVerifiedGreen(report)) {
220
+ return { mined: false, protectionsWritten: 0, exemplarsWritten: [], protectedTargets: [] };
221
+ }
222
+ const nowIso = (opts.now ? opts.now() : new Date()).toISOString();
223
+ // Read the change's artifacts straight from the change dir (tolerate absence:
224
+ // a missing artifact just truncates the walk, same as the failure path).
225
+ const specTestsMd = await readFileOrUndefined(path.join(report.changeDir, 'spec-tests.md'));
226
+ const tasksMd = await readFileOrUndefined(path.join(report.changeDir, 'tasks.md'));
227
+ const designMd = await readFileOrUndefined(path.join(report.changeDir, 'design.md'));
228
+ // Walk every passing test's credit path to its producing target + section.
229
+ const walkedPaths = [];
230
+ const minedKeys = new Map();
231
+ const protect = (targetId, section) => {
232
+ const key = `${targetId} ${section}`;
233
+ if (!minedKeys.has(key))
234
+ minedKeys.set(key, { targetId, section });
235
+ };
236
+ if (specTestsMd) {
237
+ for (const testFile of passingTestFilesFromMatrix(specTestsMd)) {
238
+ const creditPath = walkCreditPath({ testId: testFile, specTestsMd, tasksMd, designMd });
239
+ const targetId = producingTargetIdForCreditPath(creditPath);
240
+ if (!targetId)
241
+ continue;
242
+ walkedPaths.push(creditPath);
243
+ const designNode = creditPath.nodes.find((n) => n.kind === 'design-section');
244
+ // The design heading is the protected SECTION; a path that ended at a
245
+ // task protects the whole tasks template (no finer address exists).
246
+ protect(targetId, designNode ? designNode.id : '*');
247
+ }
248
+ }
249
+ // Whole-target protections: a green run whose tasks were ≥80% UC-mapped
250
+ // demonstrates the design/tasks templates produced a traceable, working
251
+ // decomposition — protect them as wholes even without per-test paths.
252
+ if (report.artifacts.taskProgress.mappingRatio >= 0.8) {
253
+ if (designMd !== undefined)
254
+ protect('artifact-template:design', '*');
255
+ if (tasksMd !== undefined)
256
+ protect('artifact-template:tasks', '*');
257
+ }
258
+ const mined = [...minedKeys.values()];
259
+ if (mined.length === 0) {
260
+ return { mined: true, protectionsWritten: 0, exemplarsWritten: [], protectedTargets: [] };
261
+ }
262
+ // MERGE with the persisted file: dedupe by targetId+section, increment
263
+ // occurrences once per mining call, track source changes uniquely.
264
+ const existing = await readAllProtections(projectRoot);
265
+ for (const m of mined) {
266
+ const found = existing.find((p) => p.targetId === m.targetId && p.section === m.section);
267
+ if (found) {
268
+ found.occurrences += 1;
269
+ if (!found.sourceChanges.includes(report.changeName)) {
270
+ found.sourceChanges.push(report.changeName);
271
+ }
272
+ found.lastObservedAt = nowIso;
273
+ }
274
+ else {
275
+ existing.push({
276
+ targetId: m.targetId,
277
+ section: m.section,
278
+ sourceChanges: [report.changeName],
279
+ occurrences: 1,
280
+ lastObservedAt: nowIso,
281
+ });
282
+ }
283
+ }
284
+ // Best-effort persist (health-baseline.ts pattern): a failed side-write must
285
+ // never fail the learn/episode run that triggered the mining.
286
+ let protectionsWritten = 0;
287
+ try {
288
+ const file = protectionsPath(projectRoot);
289
+ await fs.mkdir(path.dirname(file), { recursive: true });
290
+ const payload = { version: 1, protections: existing };
291
+ await fs.writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
292
+ protectionsWritten = mined.length;
293
+ }
294
+ catch {
295
+ // swallowed: side-write only
296
+ }
297
+ // EXEMPLARS: per protected target, persist the REAL excerpts the walked
298
+ // paths carried (the substrate the proposer's EXEMPLARS block quotes).
299
+ const exemplarsWritten = [];
300
+ const protectedTargets = [...new Set(mined.map((m) => m.targetId))];
301
+ const loss = report.fitnessSample?.loss?.loss;
302
+ for (const targetId of protectedTargets) {
303
+ const sections = [];
304
+ const seenExcerpts = new Set();
305
+ const pushExcerpt = (label, text, cap) => {
306
+ if (!text)
307
+ return;
308
+ const capped = capText(text, cap);
309
+ if (capped.length === 0 || seenExcerpts.has(capped))
310
+ return;
311
+ seenExcerpts.add(capped);
312
+ sections.push(`## ${label}\n\n${capped}`);
313
+ };
314
+ if (targetId === 'artifact-template:design') {
315
+ for (const p of walkedPaths) {
316
+ const design = p.nodes.find((n) => n.kind === 'design-section');
317
+ if (design)
318
+ pushExcerpt(`design §"${design.id}"`, design.excerpt, DESIGN_EXCERPT_CAP);
319
+ }
320
+ // One task block for context: the first walked path's task hop.
321
+ const task = walkedPaths.flatMap((p) => p.nodes).find((n) => n.kind === 'task');
322
+ if (task)
323
+ pushExcerpt(`task ${task.id}`, task.excerpt, TASK_EXCERPT_CAP);
324
+ }
325
+ else if (targetId === 'artifact-template:tasks') {
326
+ for (const p of walkedPaths) {
327
+ const task = p.nodes.find((n) => n.kind === 'task');
328
+ if (task)
329
+ pushExcerpt(`task ${task.id}`, task.excerpt, TASK_EXCERPT_CAP);
330
+ }
331
+ }
332
+ if (sections.length === 0)
333
+ continue;
334
+ const header = [
335
+ `# Exemplar: ${report.changeName}`,
336
+ '',
337
+ `- recorded: ${nowIso}`,
338
+ `- loss: ${typeof loss === 'number' ? loss.toFixed(3) : 'unmeasured'}`,
339
+ '',
340
+ ].join('\n');
341
+ const dir = exemplarDir(projectRoot, targetId);
342
+ const file = path.join(dir, `${report.changeName}.md`);
343
+ try {
344
+ await fs.mkdir(dir, { recursive: true });
345
+ await fs.writeFile(file, `${header}\n${sections.join('\n\n')}\n`, 'utf8');
346
+ exemplarsWritten.push(file);
347
+ // Retention: keep only the 3 most-recent exemplar files per target
348
+ // (mtime desc with filename-desc tiebreak — deterministic even when a
349
+ // fast filesystem stamps identical mtimes).
350
+ const all = await listExemplarFiles(projectRoot, targetId);
351
+ for (const stale of all.slice(EXEMPLARS_KEPT_PER_TARGET)) {
352
+ await fs.rm(stale, { force: true });
353
+ }
354
+ }
355
+ catch {
356
+ // swallowed: side-write only
357
+ }
358
+ }
359
+ return { mined: true, protectionsWritten, exemplarsWritten, protectedTargets };
360
+ }
361
+ //# sourceMappingURL=success-channel.js.map
@@ -24,6 +24,15 @@ export interface TargetEvolutionPolicy {
24
24
  default: 'frozen' | 'evolvable';
25
25
  /** Per-target overrides: targetId -> evolve. */
26
26
  explicit: Map<string, boolean>;
27
+ /**
28
+ * The evolution-focus switch (config `selfEvolution.focus`, default ON;
29
+ * `learn --no-focus` turns it off per-run). When ON and the policy is
30
+ * frozen-by-default with explicit evolvable targets, learn re-aims a change's
31
+ * otherwise-dropped frozen-kind signals at those targets as a synthesized
32
+ * `origin: 'policy-focus'` hint instead of silently dropping them. Optional
33
+ * so hand-built policy literals stay valid; absent ⇒ enabled.
34
+ */
35
+ focusEnabled?: boolean;
27
36
  /** Provenance, for diagnostics / a future `status` view. */
28
37
  source: {
29
38
  config: boolean;
@@ -45,6 +54,8 @@ export declare function resolveTargetEvolutionPolicy(input: {
45
54
  config?: ProjectConfig | null;
46
55
  evolveTarget?: string;
47
56
  freezeTarget?: string;
57
+ /** CLI focus override (`learn --no-focus` ⇒ false). Beats config when set. */
58
+ focus?: boolean;
48
59
  }): TargetEvolutionPolicy;
49
60
  /**
50
61
  * The single decision used by every enforcement point. Does NOT account for the
@@ -45,6 +45,8 @@ export function resolveTargetEvolutionPolicy(input) {
45
45
  return {
46
46
  default: dflt,
47
47
  explicit,
48
+ // CLI override (--no-focus) > config selfEvolution.focus > default ON.
49
+ focusEnabled: input.focus ?? se?.focus ?? true,
48
50
  source: { config: !!se, cliEvolve: input.evolveTarget, cliFreeze: input.freezeTarget },
49
51
  };
50
52
  }
@@ -189,12 +189,10 @@ export function classifyEvolvablePart(filePath) {
189
189
  if (!file)
190
190
  return [];
191
191
  const parts = [];
192
- if (file === 'src/core/self-evolution/template-variants.ts' ||
193
- file.startsWith('.synergyspec-selfevolving/self-evolution/templates/') ||
194
- file.startsWith('.synergyspec-selfevolving/self-evolution/template-variants') ||
192
+ if (file.startsWith('.synergyspec-selfevolving/self-evolution/templates/') ||
195
193
  file.startsWith('schemas/') ||
196
194
  /\/templates\//.test(file)) {
197
- parts.push('template-variants');
195
+ parts.push('artifact-templates');
198
196
  }
199
197
  if (file === 'src/core/self-evolution/archive-memory.ts') {
200
198
  parts.push('archive-memory');
@@ -212,15 +210,6 @@ export function classifyEvolvablePart(filePath) {
212
210
  file.startsWith('src/memory/extraction/')) {
213
211
  parts.push('runtime-memory');
214
212
  }
215
- if (file.startsWith('evolve/src/evolution/') ||
216
- file.startsWith('evolve/src/proposer/') ||
217
- file.startsWith('evolve/src/mutators/') ||
218
- file.startsWith('evolve/src/variant/') ||
219
- file.startsWith('evolve/src/materialize/') ||
220
- file.startsWith('evolve/src/benchmark/') ||
221
- file === 'src/commands/evolve.ts') {
222
- parts.push('dgm-harness');
223
- }
224
213
  if (file === 'src/core/self-evolution/tool-evolution.ts' ||
225
214
  file.startsWith('evolve/src/safety/') ||
226
215
  file.startsWith('evolve/test/safety/')) {
@@ -9,12 +9,15 @@ export declare const VERDICT_FILE = "verdict.json";
9
9
  * - `gate-failed` — the static gate refused it.
10
10
  * - `declined` — an auto-promote predicate said no (non-terminal: the
11
11
  * candidate may still be retried later).
12
- * - `outcompeted` — lost the GA ranking to a sibling variant for the same
13
- * target (advisory; the candidate's status is untouched).
14
12
  */
15
- export type CandidateVerdictKind = 'promoted' | 'rolled-back' | 'rejected' | 'gate-failed' | 'declined' | 'outcompeted';
16
- /** Who/what reached the verdict — for provenance in the trajectory block. */
17
- export type CandidateVerdictDecider = 'human' | 'auto-evolve' | 'evolve-from-edits' | 'evolve-outer-loop' | 'static-gate';
13
+ export type CandidateVerdictKind = 'promoted' | 'rolled-back' | 'rejected' | 'gate-failed' | 'declined';
14
+ /**
15
+ * Who/what reached the verdict for provenance in the trajectory block.
16
+ * Internal enum, not a user-facing capability. (Legacy verdict.json files may
17
+ * carry a now-removed `'auto-evolve'` decider; the unchecked cast on read keeps
18
+ * those readable.)
19
+ */
20
+ export type CandidateVerdictDecider = 'human' | 'evolve-from-edits' | 'static-gate';
18
21
  export interface CandidateVerdictRecord {
19
22
  verdict: CandidateVerdictKind;
20
23
  /** ISO-8601 UTC timestamp the verdict was reached (caller-supplied). */
@@ -2,11 +2,9 @@
2
2
  * Per-candidate promotion VERDICT (the backward-pass label the proposer reads).
3
3
  *
4
4
  * `fitness-record.jsonl` records HOW a candidate scored; this sidecar records
5
- * what the loop DECIDED about it — promoted, rejected, outcompeted, etc. — plus
6
- * the loss it carried at decision time and the baseline it was judged against.
7
- * The optimization-trajectory block ({@link import('./trajectory.js')}) renders
8
- * these labels so the proposer can learn from prior accepted/rejected attempts
9
- * (OPRO/AlphaEvolve: scored history of past solutions in the meta-prompt).
5
+ * what the loop DECIDED about it — promoted, rejected, etc. — plus the loss it
6
+ * carried at decision time and the baseline it was judged against, so the
7
+ * manual promote / evolve-from-edits path can learn from prior decisions.
10
8
  *
11
9
  * A candidate has exactly ONE current verdict, so this is a single JSON object
12
10
  * (last-write-wins), NOT an append-only log — the full transition history
@@ -37,8 +35,7 @@ function isValidVerdictKind(value) {
37
35
  value === 'rolled-back' ||
38
36
  value === 'rejected' ||
39
37
  value === 'gate-failed' ||
40
- value === 'declined' ||
41
- value === 'outcompeted');
38
+ value === 'declined');
42
39
  }
43
40
  /**
44
41
  * Write (or overwrite) a candidate's current verdict. Atomic tmp+rename inside
@@ -23,4 +23,5 @@ export { getCiSkillTemplate, getOpsxCiCommandTemplate } from './workflows/ci.js'
23
23
  export { getOpsxProposeSkillTemplate, getOpsxProposeCommandTemplate } from './workflows/propose.js';
24
24
  export { getFeedbackSkillTemplate } from './workflows/feedback.js';
25
25
  export { getCompareImagesSkillTemplate } from './workflows/compare-images.js';
26
+ export { getSelfEvolvingSkillTemplate } from './workflows/self-evolving.js';
26
27
  //# sourceMappingURL=skill-templates.d.ts.map