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.
- package/README.md +50 -19
- package/dist/commands/learn.d.ts +12 -1
- package/dist/commands/learn.js +373 -31
- package/dist/commands/self-evolution-episode.d.ts +177 -0
- package/dist/commands/self-evolution-episode.js +423 -0
- package/dist/commands/self-evolution.d.ts +12 -190
- package/dist/commands/self-evolution.js +179 -786
- package/dist/commands/workflow/status.js +3 -1
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +0 -58
- package/dist/core/artifact-graph/instruction-loader.d.ts +2 -4
- package/dist/core/artifact-graph/instruction-loader.js +3 -31
- package/dist/core/config-prompts.js +4 -0
- package/dist/core/fitness/health/health-metrics.d.ts +26 -56
- package/dist/core/fitness/health/health-metrics.js +19 -58
- package/dist/core/fitness/health/index.d.ts +15 -2
- package/dist/core/fitness/health/index.js +25 -1
- package/dist/core/fitness/health/local-source.d.ts +43 -4
- package/dist/core/fitness/health/local-source.js +181 -25
- package/dist/core/fitness/health/metric-source.d.ts +48 -19
- package/dist/core/fitness/health/metric-source.js +8 -18
- package/dist/core/fitness/health/resolve-source.js +4 -1
- package/dist/core/fitness/loss.d.ts +7 -7
- package/dist/core/fitness/loss.js +6 -6
- package/dist/core/fitness/sample.d.ts +10 -0
- package/dist/core/fitness/test-failures.d.ts +30 -0
- package/dist/core/fitness/test-failures.js +123 -0
- package/dist/core/learn/credit-path.d.ts +36 -0
- package/dist/core/learn/credit-path.js +198 -0
- package/dist/core/learn/trajectory-discovery.d.ts +39 -0
- package/dist/core/learn/trajectory-discovery.js +140 -0
- package/dist/core/learn.d.ts +39 -5
- package/dist/core/learn.js +131 -14
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.js +52 -1
- package/dist/core/self-evolution/candidate-fitness.d.ts +23 -1
- package/dist/core/self-evolution/candidate-fitness.js +31 -5
- package/dist/core/self-evolution/candidates.d.ts +0 -9
- package/dist/core/self-evolution/canonical-targets.d.ts +8 -4
- package/dist/core/self-evolution/canonical-targets.js +8 -4
- package/dist/core/self-evolution/critic-agent.d.ts +150 -0
- package/dist/core/self-evolution/critic-agent.js +487 -0
- package/dist/core/self-evolution/edits-contract.d.ts +53 -0
- package/dist/core/self-evolution/edits-contract.js +89 -0
- package/dist/core/self-evolution/episode-orchestrator.d.ts +197 -0
- package/dist/core/self-evolution/episode-orchestrator.js +534 -0
- package/dist/core/self-evolution/episode-store.d.ts +266 -0
- package/dist/core/self-evolution/episode-store.js +573 -0
- package/dist/core/self-evolution/evolution-switches.d.ts +1 -1
- package/dist/core/self-evolution/evolution-switches.js +5 -10
- package/dist/core/self-evolution/evolving-agent.d.ts +162 -0
- package/dist/core/self-evolution/evolving-agent.js +449 -0
- package/dist/core/self-evolution/health-baseline.d.ts +25 -6
- package/dist/core/self-evolution/health-baseline.js +30 -6
- package/dist/core/self-evolution/host-harness.d.ts +1 -2
- package/dist/core/self-evolution/host-harness.js +1 -2
- package/dist/core/self-evolution/index.d.ts +10 -6
- package/dist/core/self-evolution/index.js +19 -6
- package/dist/core/self-evolution/learn-hints.d.ts +31 -0
- package/dist/core/self-evolution/learn-hints.js +16 -0
- package/dist/core/self-evolution/learn-observation-adapter.d.ts +35 -0
- package/dist/core/self-evolution/learn-observation-adapter.js +285 -10
- package/dist/core/self-evolution/line-diff.d.ts +60 -0
- package/dist/core/self-evolution/line-diff.js +130 -0
- package/dist/core/self-evolution/policy/fs-safe.d.ts +19 -0
- package/dist/core/self-evolution/policy/fs-safe.js +89 -0
- package/dist/core/self-evolution/policy/index.d.ts +13 -0
- package/dist/core/self-evolution/policy/index.js +13 -0
- package/dist/core/self-evolution/policy/policy-store.d.ts +217 -0
- package/dist/core/self-evolution/policy/policy-store.js +774 -0
- package/dist/core/self-evolution/policy/reject-buffer.d.ts +48 -0
- package/dist/core/self-evolution/policy/reject-buffer.js +168 -0
- package/dist/core/self-evolution/promote.d.ts +1 -1
- package/dist/core/self-evolution/promote.js +6 -33
- package/dist/core/self-evolution/promotion.js +1 -2
- package/dist/core/self-evolution/proposer-agent.d.ts +41 -0
- package/dist/core/self-evolution/proposer-agent.js +94 -13
- package/dist/core/self-evolution/proposer-slice.d.ts +26 -0
- package/dist/core/self-evolution/proposer-slice.js +54 -0
- package/dist/core/self-evolution/reward-agent.d.ts +234 -0
- package/dist/core/self-evolution/reward-agent.js +564 -0
- package/dist/core/self-evolution/scope-gate.d.ts +66 -0
- package/dist/core/self-evolution/scope-gate.js +107 -0
- package/dist/core/self-evolution/success-channel.d.ts +79 -0
- package/dist/core/self-evolution/success-channel.js +361 -0
- package/dist/core/self-evolution/target-evolution.d.ts +11 -0
- package/dist/core/self-evolution/target-evolution.js +2 -0
- package/dist/core/self-evolution/tool-evolution.js +2 -13
- package/dist/core/self-evolution/verdict.d.ts +8 -5
- package/dist/core/self-evolution/verdict.js +4 -7
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflow-manifest.js +2 -0
- package/dist/core/templates/workflows/learn.d.ts +4 -2
- package/dist/core/templates/workflows/learn.js +25 -166
- package/dist/core/templates/workflows/self-evolving.d.ts +13 -0
- package/dist/core/templates/workflows/self-evolving.js +127 -0
- package/dist/core/trajectory/facts.d.ts +16 -0
- package/dist/core/trajectory/facts.js +12 -4
- package/dist/core/trajectory/skeleton.d.ts +43 -0
- package/dist/core/trajectory/skeleton.js +239 -0
- package/dist/dashboard/data.d.ts +25 -51
- package/dist/dashboard/data.js +68 -180
- package/dist/dashboard/react-client.js +458 -503
- package/dist/dashboard/react-styles.js +3 -3
- package/dist/dashboard/server.js +23 -17
- package/dist/ui/ascii-patterns.d.ts +7 -15
- package/dist/ui/ascii-patterns.js +123 -54
- package/dist/ui/welcome-screen.d.ts +0 -14
- package/dist/ui/welcome-screen.js +16 -35
- package/package.json +3 -1
- package/scripts/code-health.py +1066 -638
- package/scripts/slop_rules.yaml +2151 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Why the attempted evolve step was rejected. */
|
|
2
|
+
export type RejectBufferReason = 'bad-advantage' | 'human-reject';
|
|
3
|
+
/** Compact description of the rejected edit (the files themselves rolled back). */
|
|
4
|
+
export interface RejectBufferEditSummary {
|
|
5
|
+
/** Repo-relative POSIX paths the rejected edit touched. */
|
|
6
|
+
files: string[];
|
|
7
|
+
linesAdded: number;
|
|
8
|
+
linesRemoved: number;
|
|
9
|
+
/** Short excerpt of the edit's rationale, for the next episode's context. */
|
|
10
|
+
rationaleExcerpt: string;
|
|
11
|
+
}
|
|
12
|
+
/** One line of the 否决缓冲 (`reject-buffer.ndjson`). */
|
|
13
|
+
export interface RejectBufferEntry {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
/** ISO-8601 UTC timestamp the rejection was recorded. */
|
|
16
|
+
at: string;
|
|
17
|
+
episodeId: string;
|
|
18
|
+
targetId: string;
|
|
19
|
+
/** The lineage version the rejected edit was based on. */
|
|
20
|
+
fromVersion: number;
|
|
21
|
+
/** The (now rolled-back) version the rejected edit had advanced to. */
|
|
22
|
+
toVersion: number;
|
|
23
|
+
/** advantage = reward(主臂) − reward(基线臂); null when not measured. */
|
|
24
|
+
advantage: number | null;
|
|
25
|
+
rewardMain: number | null;
|
|
26
|
+
rewardBaseline: number | null;
|
|
27
|
+
/** The 文本梯度 textual gradient the rejected edit was acting on. */
|
|
28
|
+
textualGradientTried: string;
|
|
29
|
+
editSummary: RejectBufferEditSummary;
|
|
30
|
+
reason: RejectBufferReason;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Append one rejection to the 否决缓冲. Fail-closed: an invalid entry throws
|
|
34
|
+
* and nothing is written. Returns the buffer's path.
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendRejectBufferEntry(repoRoot: string, entry: RejectBufferEntry): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Read one target's rejections, newest LAST (file/append order). `limit`
|
|
39
|
+
* returns only the most recent N (still newest last). Returns `[]` when the
|
|
40
|
+
* buffer does not exist; malformed lines are skipped best-effort.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readRejectBuffer(repoRoot: string, targetId: string, limit?: number): Promise<RejectBufferEntry[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Read EVERY target's rejections, newest LAST (file/append order). Returns
|
|
45
|
+
* `[]` when the buffer does not exist; malformed lines are skipped best-effort.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readRejectBufferAll(repoRoot: string): Promise<RejectBufferEntry[]>;
|
|
48
|
+
//# sourceMappingURL=reject-buffer.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 否决缓冲 reject-buffer — the append-only record of evolve attempts the loop
|
|
3
|
+
* said NO to: a bad advantage measured by the 奖励智能体 REWARD AGENT
|
|
4
|
+
* (advantage = reward(主臂) − reward(基线臂) came back non-positive), or a
|
|
5
|
+
* human reject. The 演进智能体 EVOLVING AGENT reads this before
|
|
6
|
+
* optimizer.step so it does not retry a 文本梯度 textual gradient that
|
|
7
|
+
* already lost — the rejected edit's summary stays as evidence instead of
|
|
8
|
+
* being silently discarded with the rolled-back files.
|
|
9
|
+
*
|
|
10
|
+
* One NDJSON line per rejection in
|
|
11
|
+
* `<repoRoot>/.synergyspec-selfevolving/self-evolution/policy/reject-buffer.ndjson`,
|
|
12
|
+
* newest last. Appends are fail-closed (an invalid entry throws and writes
|
|
13
|
+
* nothing); reads skip malformed lines best-effort, matching the repo's other
|
|
14
|
+
* ndjson readers.
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { resolvePolicyLayout } from './policy-store.js';
|
|
19
|
+
import { appendFileDurable } from './fs-safe.js';
|
|
20
|
+
function isFiniteNumberOrNull(value) {
|
|
21
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
22
|
+
}
|
|
23
|
+
function isNonNegativeInteger(value) {
|
|
24
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Describe the FIRST shape problem of a would-be entry, or `null` when it is
|
|
28
|
+
* a valid {@link RejectBufferEntry}. Shared by the fail-closed append and the
|
|
29
|
+
* skip-corrupt-lines read.
|
|
30
|
+
*/
|
|
31
|
+
function rejectBufferEntryProblem(value) {
|
|
32
|
+
if (!value || typeof value !== 'object')
|
|
33
|
+
return 'entry must be an object';
|
|
34
|
+
const e = value;
|
|
35
|
+
if (e.schemaVersion !== 1)
|
|
36
|
+
return 'schemaVersion must be 1';
|
|
37
|
+
if (typeof e.at !== 'string' || e.at.length === 0)
|
|
38
|
+
return 'at must be an ISO-8601 string';
|
|
39
|
+
if (typeof e.episodeId !== 'string' || e.episodeId.length === 0) {
|
|
40
|
+
return 'episodeId must be a non-empty string';
|
|
41
|
+
}
|
|
42
|
+
if (typeof e.targetId !== 'string' || e.targetId.length === 0) {
|
|
43
|
+
return 'targetId must be a non-empty string';
|
|
44
|
+
}
|
|
45
|
+
if (!isNonNegativeInteger(e.fromVersion))
|
|
46
|
+
return 'fromVersion must be a non-negative integer';
|
|
47
|
+
if (!isNonNegativeInteger(e.toVersion))
|
|
48
|
+
return 'toVersion must be a non-negative integer';
|
|
49
|
+
if (!isFiniteNumberOrNull(e.advantage))
|
|
50
|
+
return 'advantage must be a finite number or null';
|
|
51
|
+
if (!isFiniteNumberOrNull(e.rewardMain))
|
|
52
|
+
return 'rewardMain must be a finite number or null';
|
|
53
|
+
if (!isFiniteNumberOrNull(e.rewardBaseline)) {
|
|
54
|
+
return 'rewardBaseline must be a finite number or null';
|
|
55
|
+
}
|
|
56
|
+
if (typeof e.textualGradientTried !== 'string') {
|
|
57
|
+
return 'textualGradientTried must be a string (the 文本梯度 textual gradient that was attempted)';
|
|
58
|
+
}
|
|
59
|
+
const summary = e.editSummary;
|
|
60
|
+
if (!summary || typeof summary !== 'object')
|
|
61
|
+
return 'editSummary must be an object';
|
|
62
|
+
if (!Array.isArray(summary.files) || summary.files.some((f) => typeof f !== 'string')) {
|
|
63
|
+
return 'editSummary.files must be string[]';
|
|
64
|
+
}
|
|
65
|
+
if (!isNonNegativeInteger(summary.linesAdded)) {
|
|
66
|
+
return 'editSummary.linesAdded must be a non-negative integer';
|
|
67
|
+
}
|
|
68
|
+
if (!isNonNegativeInteger(summary.linesRemoved)) {
|
|
69
|
+
return 'editSummary.linesRemoved must be a non-negative integer';
|
|
70
|
+
}
|
|
71
|
+
if (typeof summary.rationaleExcerpt !== 'string') {
|
|
72
|
+
return 'editSummary.rationaleExcerpt must be a string';
|
|
73
|
+
}
|
|
74
|
+
if (e.reason !== 'bad-advantage' && e.reason !== 'human-reject') {
|
|
75
|
+
return "reason must be 'bad-advantage' or 'human-reject'";
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append one rejection to the 否决缓冲. Fail-closed: an invalid entry throws
|
|
81
|
+
* and nothing is written. Returns the buffer's path.
|
|
82
|
+
*/
|
|
83
|
+
export async function appendRejectBufferEntry(repoRoot, entry) {
|
|
84
|
+
const problem = rejectBufferEntryProblem(entry);
|
|
85
|
+
if (problem !== null) {
|
|
86
|
+
throw new Error(`Invalid 否决缓冲 reject-buffer entry: ${problem}`);
|
|
87
|
+
}
|
|
88
|
+
const layout = resolvePolicyLayout(path.resolve(repoRoot));
|
|
89
|
+
await fs.mkdir(layout.baseDir, { recursive: true });
|
|
90
|
+
// Durable append: fsync the fd so a host crash cannot lose a rollback's
|
|
91
|
+
// reject-buffer record a later separate process (resumeEpisode) relies on.
|
|
92
|
+
await appendFileDurable(layout.rejectBufferPath, `${JSON.stringify(entry)}\n`);
|
|
93
|
+
return layout.rejectBufferPath;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Read one target's rejections, newest LAST (file/append order). `limit`
|
|
97
|
+
* returns only the most recent N (still newest last). Returns `[]` when the
|
|
98
|
+
* buffer does not exist; malformed lines are skipped best-effort.
|
|
99
|
+
*/
|
|
100
|
+
export async function readRejectBuffer(repoRoot, targetId, limit) {
|
|
101
|
+
const layout = resolvePolicyLayout(path.resolve(repoRoot));
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = await fs.readFile(layout.rejectBufferPath, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err.code === 'ENOENT')
|
|
108
|
+
return [];
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (trimmed.length === 0)
|
|
115
|
+
continue;
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(trimmed);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (rejectBufferEntryProblem(parsed) !== null)
|
|
124
|
+
continue;
|
|
125
|
+
const entry = parsed;
|
|
126
|
+
if (entry.targetId === targetId)
|
|
127
|
+
entries.push(entry);
|
|
128
|
+
}
|
|
129
|
+
if (limit === undefined)
|
|
130
|
+
return entries;
|
|
131
|
+
if (limit <= 0)
|
|
132
|
+
return [];
|
|
133
|
+
return entries.slice(-limit);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read EVERY target's rejections, newest LAST (file/append order). Returns
|
|
137
|
+
* `[]` when the buffer does not exist; malformed lines are skipped best-effort.
|
|
138
|
+
*/
|
|
139
|
+
export async function readRejectBufferAll(repoRoot) {
|
|
140
|
+
const layout = resolvePolicyLayout(path.resolve(repoRoot));
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = await fs.readFile(layout.rejectBufferPath, 'utf8');
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err.code === 'ENOENT')
|
|
147
|
+
return [];
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
const entries = [];
|
|
151
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (trimmed.length === 0)
|
|
154
|
+
continue;
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(trimmed);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (rejectBufferEntryProblem(parsed) !== null)
|
|
163
|
+
continue;
|
|
164
|
+
entries.push(parsed);
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=reject-buffer.js.map
|
|
@@ -99,7 +99,7 @@ export interface AutoPromoteDecision {
|
|
|
99
99
|
reason: string;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Pure auto-promote predicate for one-button
|
|
102
|
+
* Pure auto-promote predicate for one-button promote. The static gate +
|
|
103
103
|
* per-target switch are hard prerequisites; the fitness comparison is the
|
|
104
104
|
* regression guard.
|
|
105
105
|
*
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { promises as fs } from 'node:fs';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
|
-
import * as crypto from 'node:crypto';
|
|
27
26
|
import { GATE_DEFINING_FILES } from './candidate-gates.js';
|
|
27
|
+
import { assertWithinRepo, writeFileAtomic } from './policy/fs-safe.js';
|
|
28
28
|
import { resolveTargetLocalFiles } from './local-targets.js';
|
|
29
29
|
import { isCanonicalTargetEvolvable, } from './target-evolution.js';
|
|
30
30
|
import { readCandidatePackage, updateCandidateStatus, } from './candidates.js';
|
|
@@ -88,7 +88,7 @@ export async function applyCandidatePromotion(layout, candidateId, opts) {
|
|
|
88
88
|
assertWithinRepo(repoRoot, abs);
|
|
89
89
|
if (!(await fileExists(abs))) {
|
|
90
90
|
throw new Error(`Refusing to promote ${candidateId}: target file does not exist on disk: ${rel} ` +
|
|
91
|
-
`(
|
|
91
|
+
`(self-evolution only edits existing local files).`);
|
|
92
92
|
}
|
|
93
93
|
resolved.push({ rel, abs, content: edit.content });
|
|
94
94
|
}
|
|
@@ -143,11 +143,11 @@ export async function applyCandidatePromotion(layout, candidateId, opts) {
|
|
|
143
143
|
// Lifecycle bridge: ready-for-eval -> eval-passed -> promoted.
|
|
144
144
|
if (startStatus === 'ready-for-eval') {
|
|
145
145
|
await updateStatus(layout, candidateId, 'eval-passed', {
|
|
146
|
-
rationale: '
|
|
146
|
+
rationale: 'promote: static gate stands in for the full eval suite (MVP)',
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
promoted = await updateStatus(layout, candidateId, 'promoted', {
|
|
150
|
-
rationale: `
|
|
150
|
+
rationale: `promote: applied ${appliedFiles.length} file(s) to canonical targets [${candidate.targetIds.join(', ')}]`,
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
catch (err) {
|
|
@@ -194,7 +194,7 @@ export async function rollbackCandidatePromotion(layout, candidateId, opts) {
|
|
|
194
194
|
restoredFiles.push(entry.relPath);
|
|
195
195
|
}
|
|
196
196
|
const rolled = await updateCandidateStatus(layout, candidateId, 'rolled-back', {
|
|
197
|
-
rationale: '
|
|
197
|
+
rationale: 'rollback: restored pre-promotion snapshot',
|
|
198
198
|
});
|
|
199
199
|
return { candidateId, status: rolled.status, restoredFiles };
|
|
200
200
|
}
|
|
@@ -205,7 +205,7 @@ export async function rollbackCandidatePromotion(layout, candidateId, opts) {
|
|
|
205
205
|
*/
|
|
206
206
|
export const DEFAULT_HEALTH_REGRESSION_MARGIN = 0.05;
|
|
207
207
|
/**
|
|
208
|
-
* Pure auto-promote predicate for one-button
|
|
208
|
+
* Pure auto-promote predicate for one-button promote. The static gate +
|
|
209
209
|
* per-target switch are hard prerequisites; the fitness comparison is the
|
|
210
210
|
* regression guard.
|
|
211
211
|
*
|
|
@@ -377,31 +377,4 @@ async function fileExists(abs) {
|
|
|
377
377
|
return false;
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
|
-
/** Throw if `abs` resolves outside `repoRoot`. */
|
|
381
|
-
function assertWithinRepo(repoRoot, abs) {
|
|
382
|
-
const base = path.resolve(repoRoot);
|
|
383
|
-
const target = path.resolve(abs);
|
|
384
|
-
const rel = path.relative(base, target);
|
|
385
|
-
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
386
|
-
throw new Error(`Refusing to write outside repo root. root=${base} target=${target}`);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/** Atomic write via sibling tmp file + rename; cleans up on rename failure. */
|
|
390
|
-
async function writeFileAtomic(abs, content) {
|
|
391
|
-
const dir = path.dirname(abs);
|
|
392
|
-
const tmp = path.join(dir, `.${path.basename(abs)}.tmp-${crypto.randomBytes(4).toString('hex')}`);
|
|
393
|
-
await fs.writeFile(tmp, content, 'utf8');
|
|
394
|
-
try {
|
|
395
|
-
await fs.rename(tmp, abs);
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
try {
|
|
399
|
-
await fs.rm(tmp, { force: true });
|
|
400
|
-
}
|
|
401
|
-
catch {
|
|
402
|
-
// ignore
|
|
403
|
-
}
|
|
404
|
-
throw err;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
380
|
//# sourceMappingURL=promote.js.map
|
|
@@ -28,8 +28,7 @@ import * as crypto from 'node:crypto';
|
|
|
28
28
|
import { readCandidatePackage, setCandidateArtifactPath, } from './candidates.js';
|
|
29
29
|
import { lookupCanonicalTarget, } from './canonical-targets.js';
|
|
30
30
|
import { candidateEvalReportSchema } from './eval-report.js';
|
|
31
|
-
import { readCandidateFitness, } from './candidate-fitness.js';
|
|
32
|
-
import { readPromotedBaselineLoss } from './ga-selection.js';
|
|
31
|
+
import { readCandidateFitness, readPromotedBaselineLoss, } from './candidate-fitness.js';
|
|
33
32
|
import { DEFAULT_LOSS_REGRESSION_MARGIN } from './promote.js';
|
|
34
33
|
/** Risk tier ordering used when comparing candidate risk to target risk. */
|
|
35
34
|
const RISK_ORDER = {
|
|
@@ -61,6 +61,26 @@ export interface CanonicalProposeInput {
|
|
|
61
61
|
* sibling variants drafted from the same evidence diverge. Omitted when absent.
|
|
62
62
|
*/
|
|
63
63
|
variantAngle?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Pre-rendered DO-NOT-PRUNE block from the SUCCESS CHANNEL (see
|
|
66
|
+
* {@link import('./success-channel.js').renderDoNotPruneBlock}): target
|
|
67
|
+
* sections implicated in verified-PASSING runs. Rendered between EVIDENCE and
|
|
68
|
+
* the improvement angle so the constraint is read BEFORE the angle commits the
|
|
69
|
+
* model to (e.g.) pruning. Omitted when empty.
|
|
70
|
+
*/
|
|
71
|
+
doNotPrune?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Bounded REAL excerpts from the success-channel exemplar files (what the
|
|
74
|
+
* protected sections actually look like in passing changes). Omitted when empty.
|
|
75
|
+
*/
|
|
76
|
+
exemplarsContext?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Pre-rendered CREDIT-PATH SLICE (see {@link import('./proposer-slice.js')}):
|
|
79
|
+
* the REAL artifact text along each failing signal's credit path (matrix row,
|
|
80
|
+
* task block, design section), so the proposer reads the actual documents —
|
|
81
|
+
* not just quotes about them. Rendered right after EVIDENCE; omitted when empty.
|
|
82
|
+
*/
|
|
83
|
+
sliceContext?: string;
|
|
64
84
|
/** Injected for tests; defaults to node's spawn. */
|
|
65
85
|
spawn?: typeof nodeSpawn;
|
|
66
86
|
/** Override the agent binary; defaults to env or 'claude'. */
|
|
@@ -137,5 +157,26 @@ export declare function renderUnifiedDiff(relPath: string, oldContent: string, n
|
|
|
137
157
|
* the CLI catches these and falls back to the placeholder candidate.
|
|
138
158
|
*/
|
|
139
159
|
export declare function runCanonicalProposerAgent(input: CanonicalProposeInput): Promise<CanonicalProposeOutput>;
|
|
160
|
+
/**
|
|
161
|
+
* Repair-loop variant of {@link runCanonicalProposerAgent}, used by the opt-in
|
|
162
|
+
* `--agent` path (the unidecode run's real defect: the headless proposer can
|
|
163
|
+
* fail to wrap a large markdown file as valid JSON inside the ```json:patch
|
|
164
|
+
* block, and the old blind same-prompt retry could only fail the same way).
|
|
165
|
+
*
|
|
166
|
+
* On a VALIDATION failure (`CanonicalProposerOutputInvalid` — malformed JSON,
|
|
167
|
+
* wrong block count, scope/frozen violations) it re-prompts with the concrete
|
|
168
|
+
* validation error appended, up to `maxRepairAttempts` repairs (default 2 ⇒ at
|
|
169
|
+
* most 3 parse attempts). After a successful parse it runs the cheap
|
|
170
|
+
* deterministic in-loop checks (the scope/frozen validation already ran inside
|
|
171
|
+
* the parser; a no-op diff — every edit byte-identical to the current file —
|
|
172
|
+
* is fed back as a repairable failure too, since an "edit" that changes
|
|
173
|
+
* nothing cannot drive loss down). Exhaustion throws the last error, so the
|
|
174
|
+
* CLI's existing placeholder fallback and skip-recording stay the terminal
|
|
175
|
+
* state. Invocation errors (agent crash) are NOT repaired — they propagate as
|
|
176
|
+
* before. The default `--from-edits` path never reaches this function.
|
|
177
|
+
*/
|
|
178
|
+
export declare function runCanonicalProposerAgentWithRepair(input: CanonicalProposeInput, opts?: {
|
|
179
|
+
maxRepairAttempts?: number;
|
|
180
|
+
}): Promise<CanonicalProposeOutput>;
|
|
140
181
|
export {};
|
|
141
182
|
//# sourceMappingURL=proposer-agent.d.ts.map
|
|
@@ -98,6 +98,28 @@ export function assembleCanonicalProposerPrompt(input) {
|
|
|
98
98
|
for (const b of benefits)
|
|
99
99
|
parts.push(`- ${b}`);
|
|
100
100
|
}
|
|
101
|
+
// CREDIT-PATH SLICE directly after EVIDENCE: the real artifact text along the
|
|
102
|
+
// failing path, so the evidence lines above are readable in their actual
|
|
103
|
+
// context. Omitted-when-empty (legacy hints carry no credit paths).
|
|
104
|
+
const slice = (input.sliceContext ?? '').trim();
|
|
105
|
+
if (slice.length > 0) {
|
|
106
|
+
parts.push('', '# CREDIT-PATH SLICE (real artifact text along the failing path)');
|
|
107
|
+
parts.push('Each path traces a failing signal back toward the artifact that plausibly', 'produced it (SUSPECT framing — recurrence is the confirmer). The excerpts', 'are the REAL current text at each hop.', slice);
|
|
108
|
+
}
|
|
109
|
+
// SUCCESS-CHANNEL constraints sit between EVIDENCE and the IMPROVEMENT ANGLE:
|
|
110
|
+
// the model must read what is load-bearing BEFORE an angle (especially PRUNE)
|
|
111
|
+
// commits it to deleting things. Both omitted-when-empty so prompts on runs
|
|
112
|
+
// with no green history stay byte-identical.
|
|
113
|
+
const doNotPrune = (input.doNotPrune ?? '').trim();
|
|
114
|
+
if (doNotPrune.length > 0) {
|
|
115
|
+
parts.push('', '# DO-NOT-PRUNE (load-bearing — implicated in passing runs)');
|
|
116
|
+
parts.push('The sections below are implicated in verified-PASSING runs. They must not be', 'deleted or hollowed out EVEN UNDER the PRUNE improvement angle — prune elsewhere.', doNotPrune);
|
|
117
|
+
}
|
|
118
|
+
const exemplars = (input.exemplarsContext ?? '').trim();
|
|
119
|
+
if (exemplars.length > 0) {
|
|
120
|
+
parts.push('', '# EXEMPLARS (from passing runs)');
|
|
121
|
+
parts.push(exemplars);
|
|
122
|
+
}
|
|
101
123
|
const angle = (input.variantAngle ?? '').trim();
|
|
102
124
|
if (angle.length > 0) {
|
|
103
125
|
parts.push('', '# IMPROVEMENT ANGLE (this variant)');
|
|
@@ -215,19 +237,8 @@ async function invokeCanonicalProposer(opts) {
|
|
|
215
237
|
return second.stdout;
|
|
216
238
|
throw new CanonicalProposerInvocationError(second.stderr || first.stderr);
|
|
217
239
|
}
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
* candidate diff. Throws CanonicalProposerNoOp / *Invalid / *InvocationError —
|
|
221
|
-
* the CLI catches these and falls back to the placeholder candidate.
|
|
222
|
-
*/
|
|
223
|
-
export async function runCanonicalProposerAgent(input) {
|
|
224
|
-
const prompt = assembleCanonicalProposerPrompt(input);
|
|
225
|
-
const stdout = await invokeCanonicalProposer({
|
|
226
|
-
prompt,
|
|
227
|
-
spawn: input.spawn,
|
|
228
|
-
binary: input.binary,
|
|
229
|
-
});
|
|
230
|
-
const parsed = parseCanonicalProposerResponse(stdout, input.target.files);
|
|
240
|
+
/** Shared output synthesis for the single-shot and repair-loop entry points. */
|
|
241
|
+
function buildProposeOutput(input, parsed) {
|
|
231
242
|
const oldByPath = new Map(input.currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
232
243
|
const diffPatch = parsed.edits
|
|
233
244
|
.map((e) => renderUnifiedDiff(e.relPath, oldByPath.get(e.relPath) ?? '', e.content))
|
|
@@ -242,4 +253,74 @@ export async function runCanonicalProposerAgent(input) {
|
|
|
242
253
|
edits: parsed.edits,
|
|
243
254
|
};
|
|
244
255
|
}
|
|
256
|
+
/** True when every parsed edit is byte-identical to the current on-disk file. */
|
|
257
|
+
function isNoOpDiff(input, parsed) {
|
|
258
|
+
const oldByPath = new Map(input.currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
259
|
+
return parsed.edits.every((e) => oldByPath.get(e.relPath) === e.content);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Top-level: prompt the agent, parse + validate its edits, and synthesize a
|
|
263
|
+
* candidate diff. Throws CanonicalProposerNoOp / *Invalid / *InvocationError —
|
|
264
|
+
* the CLI catches these and falls back to the placeholder candidate.
|
|
265
|
+
*/
|
|
266
|
+
export async function runCanonicalProposerAgent(input) {
|
|
267
|
+
const prompt = assembleCanonicalProposerPrompt(input);
|
|
268
|
+
const stdout = await invokeCanonicalProposer({
|
|
269
|
+
prompt,
|
|
270
|
+
spawn: input.spawn,
|
|
271
|
+
binary: input.binary,
|
|
272
|
+
});
|
|
273
|
+
const parsed = parseCanonicalProposerResponse(stdout, input.target.files);
|
|
274
|
+
return buildProposeOutput(input, parsed);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Repair-loop variant of {@link runCanonicalProposerAgent}, used by the opt-in
|
|
278
|
+
* `--agent` path (the unidecode run's real defect: the headless proposer can
|
|
279
|
+
* fail to wrap a large markdown file as valid JSON inside the ```json:patch
|
|
280
|
+
* block, and the old blind same-prompt retry could only fail the same way).
|
|
281
|
+
*
|
|
282
|
+
* On a VALIDATION failure (`CanonicalProposerOutputInvalid` — malformed JSON,
|
|
283
|
+
* wrong block count, scope/frozen violations) it re-prompts with the concrete
|
|
284
|
+
* validation error appended, up to `maxRepairAttempts` repairs (default 2 ⇒ at
|
|
285
|
+
* most 3 parse attempts). After a successful parse it runs the cheap
|
|
286
|
+
* deterministic in-loop checks (the scope/frozen validation already ran inside
|
|
287
|
+
* the parser; a no-op diff — every edit byte-identical to the current file —
|
|
288
|
+
* is fed back as a repairable failure too, since an "edit" that changes
|
|
289
|
+
* nothing cannot drive loss down). Exhaustion throws the last error, so the
|
|
290
|
+
* CLI's existing placeholder fallback and skip-recording stay the terminal
|
|
291
|
+
* state. Invocation errors (agent crash) are NOT repaired — they propagate as
|
|
292
|
+
* before. The default `--from-edits` path never reaches this function.
|
|
293
|
+
*/
|
|
294
|
+
export async function runCanonicalProposerAgentWithRepair(input, opts = {}) {
|
|
295
|
+
const maxRepairAttempts = Math.max(0, opts.maxRepairAttempts ?? 2);
|
|
296
|
+
const basePrompt = assembleCanonicalProposerPrompt(input);
|
|
297
|
+
let feedback = null;
|
|
298
|
+
for (let attempt = 0;; attempt++) {
|
|
299
|
+
const prompt = feedback === null
|
|
300
|
+
? basePrompt
|
|
301
|
+
: `${basePrompt}\n\n# PREVIOUS ATTEMPT FAILED VALIDATION\n${feedback}\n` +
|
|
302
|
+
'Re-emit EXACTLY ONE ```json:patch fenced block of the form ' +
|
|
303
|
+
'{"rationale": string, "edits": [{"relPath", "content"}]} — each `content` ' +
|
|
304
|
+
'is the COMPLETE new file as a single valid JSON string (escape newlines and quotes).';
|
|
305
|
+
const stdout = await invokeCanonicalProposer({
|
|
306
|
+
prompt,
|
|
307
|
+
spawn: input.spawn,
|
|
308
|
+
binary: input.binary,
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const parsed = parseCanonicalProposerResponse(stdout, input.target.files);
|
|
312
|
+
if (isNoOpDiff(input, parsed)) {
|
|
313
|
+
throw new CanonicalProposerOutputInvalid('every edit is byte-identical to the current file (no-op diff) — change something the evidence implicates, or return an empty edits array to decline');
|
|
314
|
+
}
|
|
315
|
+
return buildProposeOutput(input, parsed);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (err instanceof CanonicalProposerOutputInvalid && attempt < maxRepairAttempts) {
|
|
319
|
+
feedback = err.message;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
245
326
|
//# sourceMappingURL=proposer-agent.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit-path slice builder (R6): turn the hints' `creditPaths` — the neutral
|
|
3
|
+
* walker's hops with their bounded REAL artifact excerpts — into the
|
|
4
|
+
* `# CREDIT-PATH SLICE` text the proposer reads. The proposer thereby sees the
|
|
5
|
+
* actual design-section / task / matrix-row text along the failing path,
|
|
6
|
+
* instead of only quotes about it.
|
|
7
|
+
*
|
|
8
|
+
* Deterministic: hints ordered by id, hops in path order; bounded by a
|
|
9
|
+
* per-excerpt cap and a total cap so a pathological corpus cannot flood the
|
|
10
|
+
* prompt. Pure; '' when there is nothing to render (callers omit the section
|
|
11
|
+
* entirely then, matching the prompt's omitted-when-empty convention).
|
|
12
|
+
*/
|
|
13
|
+
import type { LearnEvolutionHint } from './learn-hints.js';
|
|
14
|
+
export interface ProposerSliceCaps {
|
|
15
|
+
/** Per-excerpt character cap (default 1500). */
|
|
16
|
+
perExcerptChars: number;
|
|
17
|
+
/** Total slice character cap (default 6000). */
|
|
18
|
+
totalChars: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the slice text from a group's hints. Deduplicates identical paths
|
|
22
|
+
* (same node ids in the same order) across hints so recurring failures don't
|
|
23
|
+
* repeat the same design section.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildProposerSlice(hints: readonly LearnEvolutionHint[], caps?: Partial<ProposerSliceCaps>): string;
|
|
26
|
+
//# sourceMappingURL=proposer-slice.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DEFAULT_CAPS = { perExcerptChars: 1500, totalChars: 6000 };
|
|
2
|
+
function nodeLabel(node) {
|
|
3
|
+
const where = node.file ? ` (${node.file}${node.line ? `:${node.line}` : ''})` : '';
|
|
4
|
+
return `${node.kind} ${node.id}${where}`;
|
|
5
|
+
}
|
|
6
|
+
function capText(text, max) {
|
|
7
|
+
const t = text.trim();
|
|
8
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Render the slice for one hint's credit paths. Each hop that carries a real
|
|
12
|
+
* excerpt becomes a labeled block; hops without excerpts are listed inline.
|
|
13
|
+
*/
|
|
14
|
+
function renderPath(path, caps) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push(`path: ${path.nodes.map(nodeLabel).join(' → ')}${path.truncatedAt ? ` [truncated: ${path.truncatedAt}]` : ''}`);
|
|
17
|
+
for (const node of path.nodes) {
|
|
18
|
+
if (!node.excerpt)
|
|
19
|
+
continue;
|
|
20
|
+
lines.push(`<<${nodeLabel(node)}>>`);
|
|
21
|
+
lines.push(capText(node.excerpt, caps.perExcerptChars));
|
|
22
|
+
lines.push('<<end>>');
|
|
23
|
+
}
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the slice text from a group's hints. Deduplicates identical paths
|
|
28
|
+
* (same node ids in the same order) across hints so recurring failures don't
|
|
29
|
+
* repeat the same design section.
|
|
30
|
+
*/
|
|
31
|
+
export function buildProposerSlice(hints, caps = {}) {
|
|
32
|
+
const resolved = { ...DEFAULT_CAPS, ...caps };
|
|
33
|
+
const ordered = [...hints].sort((a, b) => a.id.localeCompare(b.id));
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const hint of ordered) {
|
|
38
|
+
for (const path of hint.creditPaths ?? []) {
|
|
39
|
+
const key = path.nodes.map((n) => `${n.kind}:${n.id}`).join('→');
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
const block = renderPath(path, resolved);
|
|
44
|
+
if (total + block.length > resolved.totalChars) {
|
|
45
|
+
blocks.push(`(slice truncated: total cap ${resolved.totalChars} chars reached)`);
|
|
46
|
+
return blocks.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
total += block.length;
|
|
49
|
+
blocks.push(block);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return blocks.join('\n\n');
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=proposer-slice.js.map
|