backpass 0.1.7 → 0.1.8
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 +39 -15
- package/package.json +1 -1
- package/src/acpx.js +142 -52
- package/src/analyze.js +4 -3
- package/src/apply/terminal.js +4 -3
- package/src/apply/writer.js +206 -19
- package/src/cli.js +3 -3
- package/src/commands/propose.js +60 -6
- package/src/commands/run.js +6 -7
- package/src/harness-invoke.js +277 -0
- package/src/prompts/annotate-preface.md +19 -0
- package/src/prompts/annotate.md +11 -6
- package/src/proposal.js +39 -8
- package/src/skills.js +156 -7
- package/src/state.js +5 -1
- package/src/subprocess.js +53 -5
- package/src/synthesize.js +311 -99
- package/src/tui/index.js +12 -0
- package/src/tui/render.js +16 -0
- package/templates/apply.html +26 -16
package/src/apply/writer.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
|
|
4
5
|
import { applyEdit, projectWithDecisions } from "../proposal.js";
|
|
5
6
|
import { memoryTextHash } from "../memory.js";
|
|
6
7
|
import { budgetGateKind, budgetStatus, formatTokens } from "../tokens.js";
|
|
7
8
|
import { recordRejection } from "../state.js";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CANONICAL_SKILLS_DIR,
|
|
11
|
+
CLAUDE_SKILLS_LINK,
|
|
12
|
+
editSkills,
|
|
13
|
+
ensureSkillsLayout,
|
|
14
|
+
removeOwnedSkillPaths,
|
|
15
|
+
writeSkill,
|
|
16
|
+
} from "../skills.js";
|
|
9
17
|
|
|
10
18
|
function acceptedSubsetBudgetFailure({ proposal, accepted, repo, capTokens, memoryText }) {
|
|
11
19
|
if (!accepted.length) return null;
|
|
@@ -98,16 +106,77 @@ function overBudgetWarning(relative, budget) {
|
|
|
98
106
|
);
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
function atomicReplace(target, text) {
|
|
110
|
+
const temp = path.join(path.dirname(target), `.${path.basename(target)}.backpass-${randomUUID()}`);
|
|
111
|
+
const mode = fs.statSync(target).mode & 0o7777;
|
|
112
|
+
let fd;
|
|
113
|
+
let ownership;
|
|
114
|
+
try {
|
|
115
|
+
fd = fs.openSync(temp, "wx");
|
|
116
|
+
const stat = fs.fstatSync(fd);
|
|
117
|
+
ownership = [{ absolute: temp, identity: { dev: stat.dev, ino: stat.ino } }];
|
|
118
|
+
fs.fchmodSync(fd, mode);
|
|
119
|
+
fs.writeFileSync(fd, text);
|
|
120
|
+
fs.fsyncSync(fd);
|
|
121
|
+
fs.closeSync(fd);
|
|
122
|
+
fd = undefined;
|
|
123
|
+
fs.renameSync(temp, target);
|
|
124
|
+
return { absolute: target, identity: ownership[0].identity, text };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (fd !== undefined) {
|
|
127
|
+
try {
|
|
128
|
+
fs.closeSync(fd);
|
|
129
|
+
} catch {
|
|
130
|
+
// Preserve the original write error.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
removeOwnedSkillPaths(ownership ?? []);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function commitStillCurrent(commit) {
|
|
139
|
+
let stat;
|
|
140
|
+
try {
|
|
141
|
+
stat = fs.lstatSync(commit.absolute);
|
|
142
|
+
if (stat.dev !== commit.identity.dev || stat.ino !== commit.identity.ino) return false;
|
|
143
|
+
return fs.readFileSync(commit.absolute, "utf8") === commit.text;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function absentParentDirectories(root, files) {
|
|
150
|
+
const directories = new Set();
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
for (let current = path.dirname(file); current !== root; current = path.dirname(current)) {
|
|
153
|
+
if (!fs.existsSync(current)) directories.add(current);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [...directories].sort((a, b) => b.length - a.length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function removeEmptyDirectories(directories) {
|
|
160
|
+
for (const directory of directories) {
|
|
161
|
+
try {
|
|
162
|
+
fs.rmdirSync(directory);
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
101
169
|
/**
|
|
102
170
|
* The only place in backpass that writes to the repo.
|
|
103
171
|
*
|
|
104
172
|
* Everything upstream is read-only analysis; a run only changes the weights here, after
|
|
105
|
-
* a human accepted specific edits.
|
|
173
|
+
* a human accepted specific edits. Five gates run before the first byte is written:
|
|
106
174
|
* the memory file must still be the file the proposal was measured against
|
|
107
175
|
* (`memoryFileSnapshot`), the accepted subset must clear the same cap/shrink budget gate as
|
|
108
|
-
* the full proposal (`budgetGateKind`),
|
|
109
|
-
* against that file's single pre-write image
|
|
110
|
-
*
|
|
176
|
+
* the full proposal (`budgetGateKind`), every accepted edit for a file must compose
|
|
177
|
+
* against that file's single pre-write image, every created skill target must still be
|
|
178
|
+
* absent, and accepted paths must resolve to distinct targets. Any of them failing writes
|
|
179
|
+
* nothing and records no rejection.
|
|
111
180
|
*
|
|
112
181
|
* A file is therefore applied all at once or not at all. Skills are written only after
|
|
113
182
|
* every accepted edit has composed, and before the files that reference them.
|
|
@@ -197,33 +266,98 @@ export function applyDecisions({ proposal, decisions, repo, state, config, dryRu
|
|
|
197
266
|
for (const id of applied) landed.add(id);
|
|
198
267
|
}
|
|
199
268
|
|
|
269
|
+
const plannedTargets = new Map();
|
|
270
|
+
const resolvedPlanned = [];
|
|
271
|
+
for (const item of planned) {
|
|
272
|
+
let resolved;
|
|
273
|
+
try {
|
|
274
|
+
resolved = fs.realpathSync(item.absolute);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
results.failed.push({ file: item.relative, error: `${item.relative} could not be resolved: ${err.message}` });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const existing = plannedTargets.get(resolved);
|
|
280
|
+
if (existing) {
|
|
281
|
+
results.failed.push({
|
|
282
|
+
file: item.relative,
|
|
283
|
+
error: `${item.relative} resolves to the same target as ${existing.relative}; nothing was written`,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const resolvedItem = { ...item, resolved };
|
|
288
|
+
plannedTargets.set(resolved, resolvedItem);
|
|
289
|
+
resolvedPlanned.push(resolvedItem);
|
|
290
|
+
}
|
|
291
|
+
|
|
200
292
|
if (results.failed.length) return results;
|
|
201
293
|
|
|
202
294
|
// Skills go in before the memory file. A skill nothing points at yet is inert, while a
|
|
203
295
|
// memory file pointing at a skill that is not there is actively wrong - so if a skill
|
|
204
296
|
// cannot be written, the files that would reference it are left alone.
|
|
205
|
-
const
|
|
206
|
-
const
|
|
297
|
+
const plannedSkills = [];
|
|
298
|
+
const skillPaths = new Set();
|
|
207
299
|
for (const edit of accepted) {
|
|
208
|
-
if (edit.kind !== "extract" || !edit.
|
|
209
|
-
|
|
300
|
+
if (edit.kind !== "extract" || !landed.has(edit.id)) continue;
|
|
301
|
+
for (const skill of editSkills(edit)) {
|
|
302
|
+
const absolute = path.join(repo.root, skill.path);
|
|
303
|
+
if (skillPaths.has(skill.path) || fs.existsSync(absolute)) {
|
|
304
|
+
results.failed.push({
|
|
305
|
+
file: skill.path,
|
|
306
|
+
edit: edit.id,
|
|
307
|
+
error: `${skill.path} already exists; nothing was written`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
skillPaths.add(skill.path);
|
|
311
|
+
plannedSkills.push({ edit, skill });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (results.failed.length) return results;
|
|
315
|
+
|
|
316
|
+
const canonical = plannedSkills.find(
|
|
317
|
+
({ skill }) => skill.path === CANONICAL_SKILLS_DIR || skill.path.startsWith(`${CANONICAL_SKILLS_DIR}/`),
|
|
318
|
+
);
|
|
319
|
+
const createdDirectoryCandidates = absentParentDirectories(repo.root, [
|
|
320
|
+
...plannedSkills.map(({ skill }) => path.join(repo.root, skill.path)),
|
|
321
|
+
...(canonical ? [path.join(repo.root, CLAUDE_SKILLS_LINK)] : []),
|
|
322
|
+
]);
|
|
323
|
+
const skillFailures = [];
|
|
324
|
+
const ownedSkillPaths = [];
|
|
325
|
+
for (const { edit, skill } of plannedSkills) {
|
|
210
326
|
try {
|
|
211
|
-
const layout = dryRun
|
|
212
|
-
|
|
213
|
-
|
|
327
|
+
const layout = dryRun
|
|
328
|
+
? { created: [], warnings: [] }
|
|
329
|
+
: writeSkill(repo.root, skill, { exclusive: true, ensureLayout: false });
|
|
330
|
+
results.skills.push({ path: skill.path, dryRun, created: layout.created });
|
|
331
|
+
if (!dryRun) {
|
|
332
|
+
ownedSkillPaths.push(...("ownership" in layout && Array.isArray(layout.ownership) ? layout.ownership : []));
|
|
333
|
+
}
|
|
214
334
|
for (const w of layout.warnings) if (!results.warnings.includes(w)) results.warnings.push(w);
|
|
215
335
|
} catch (err) {
|
|
216
|
-
skillFailures.push({ file:
|
|
336
|
+
skillFailures.push({ file: skill.path, edit: edit.id, error: err.message });
|
|
217
337
|
}
|
|
218
338
|
}
|
|
219
339
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
340
|
+
const rollbackSkills = () => {
|
|
341
|
+
const { removed, conflicts } = removeOwnedSkillPaths(ownedSkillPaths);
|
|
342
|
+
removeEmptyDirectories(createdDirectoryCandidates);
|
|
343
|
+
results.skills = [];
|
|
344
|
+
const removedPaths = removed.map((item) => item.relative).filter(Boolean);
|
|
345
|
+
if (removedPaths.length) {
|
|
223
346
|
results.failed.push({
|
|
224
|
-
error: `skill paths
|
|
347
|
+
error: `rolled back skill paths written earlier in this round: ${removedPaths.join(", ")}`,
|
|
225
348
|
});
|
|
226
349
|
}
|
|
350
|
+
for (const item of conflicts) {
|
|
351
|
+
results.failed.push({
|
|
352
|
+
file: item.relative,
|
|
353
|
+
error: `${item.relative} rollback conflict: the skill changed after this apply wrote it; left untouched`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (skillFailures.length) {
|
|
359
|
+
results.failed.push(...skillFailures);
|
|
360
|
+
rollbackSkills();
|
|
227
361
|
for (const { relative } of planned) {
|
|
228
362
|
results.failed.push({
|
|
229
363
|
file: relative,
|
|
@@ -233,16 +367,69 @@ export function applyDecisions({ proposal, decisions, repo, state, config, dryRu
|
|
|
233
367
|
return results;
|
|
234
368
|
}
|
|
235
369
|
|
|
236
|
-
|
|
370
|
+
const orderedPlanned = [...resolvedPlanned].sort((a, b) => {
|
|
371
|
+
const aMemory = a.relative === proposal.memoryFile.path;
|
|
372
|
+
const bMemory = b.relative === proposal.memoryFile.path;
|
|
373
|
+
return Number(aMemory) - Number(bMemory);
|
|
374
|
+
});
|
|
375
|
+
const committed = [];
|
|
376
|
+
const rollbackCommitted = () => {
|
|
377
|
+
for (const written of [...committed].reverse()) {
|
|
378
|
+
if (!commitStillCurrent(written.commit)) {
|
|
379
|
+
results.failed.push({
|
|
380
|
+
file: written.relative,
|
|
381
|
+
error: `${written.relative} rollback conflict: the file changed after this apply wrote it; left untouched`,
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
atomicReplace(written.commit.absolute, written.before);
|
|
387
|
+
} catch (rollbackError) {
|
|
388
|
+
results.failed.push({
|
|
389
|
+
file: written.relative,
|
|
390
|
+
error: `${written.relative} could not be rolled back: ${rollbackError.message}`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
results.written = [];
|
|
395
|
+
};
|
|
396
|
+
for (const item of orderedPlanned) {
|
|
397
|
+
const { relative, resolved, before, text, applied } = item;
|
|
237
398
|
const budget = relative === proposal.memoryFile.path ? budgetStatus(before, text, config.budgetTokens) : null;
|
|
238
399
|
|
|
239
|
-
|
|
400
|
+
let commit = null;
|
|
401
|
+
try {
|
|
402
|
+
if (!dryRun) commit = atomicReplace(resolved, text);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
results.failed.push({
|
|
405
|
+
file: relative,
|
|
406
|
+
error: `${relative} could not be written: ${err.message}`,
|
|
407
|
+
});
|
|
408
|
+
rollbackCommitted();
|
|
409
|
+
rollbackSkills();
|
|
410
|
+
return results;
|
|
411
|
+
}
|
|
412
|
+
committed.push({ ...item, commit });
|
|
240
413
|
results.written.push({ file: relative, edits: applied, budget, dryRun });
|
|
241
414
|
|
|
242
415
|
// Shrinking over several runs is the design, so this is a heading, not a failure.
|
|
243
416
|
if (budget && !budget.withinBudget) results.warnings.push(overBudgetWarning(relative, budget));
|
|
244
417
|
}
|
|
245
418
|
|
|
419
|
+
if (!dryRun && canonical) {
|
|
420
|
+
try {
|
|
421
|
+
const layout = ensureSkillsLayout(repo.root);
|
|
422
|
+
const result = results.skills.find(({ path: skillPath }) => skillPath === canonical.skill.path);
|
|
423
|
+
result.created = [...new Set([...result.created, ...layout.created])];
|
|
424
|
+
for (const w of layout.warnings) if (!results.warnings.includes(w)) results.warnings.push(w);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
results.failed.push({ file: CLAUDE_SKILLS_LINK, edit: canonical.edit.id, error: err.message });
|
|
427
|
+
rollbackCommitted();
|
|
428
|
+
rollbackSkills();
|
|
429
|
+
return results;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
246
433
|
// Rejections are remembered so the same edit is not re-proposed without new evidence.
|
|
247
434
|
if (!dryRun && rejected.length) {
|
|
248
435
|
const rejections = state.readRejections();
|
package/src/cli.js
CHANGED
|
@@ -74,7 +74,7 @@ COMMANDS
|
|
|
74
74
|
gradient descent. Never writes.
|
|
75
75
|
scan collect samples only: which transcripts belong to this repo, and how we know
|
|
76
76
|
analyze calculate loss: one cheap model call per new transcript (tier 1)
|
|
77
|
-
propose aggregate gradients, then
|
|
77
|
+
propose aggregate gradients, then high-reasoning gradient descent
|
|
78
78
|
turning the aggregated evidence into edits (tier 2)
|
|
79
79
|
apply review the proposal and write the accepted edits (the only writer)
|
|
80
80
|
status cache state, evidence counts, and the budget bar
|
|
@@ -99,10 +99,10 @@ MODELS (two-tier: cheap analysis, smart synthesis - all through acpx)
|
|
|
99
99
|
Setting an agent pins that pass and skips its ladder.
|
|
100
100
|
--analysis-agent <a> acpx agent for the per-transcript pass [auto]
|
|
101
101
|
--analysis-model <id> model id for the analysis pass (needs --analysis-agent)
|
|
102
|
-
--analysis-effort <e> reasoning effort, when
|
|
102
|
+
--analysis-effort <e> one-off reasoning effort, when supported [medium]
|
|
103
103
|
--synthesis-agent <a> acpx agent for the final proposal pass [auto]
|
|
104
104
|
--synthesis-model <id> model id for the synthesis pass (needs --synthesis-agent)
|
|
105
|
-
--synthesis-effort <e> reasoning effort for synthesis
|
|
105
|
+
--synthesis-effort <e> one-off reasoning effort for synthesis [high]
|
|
106
106
|
--no-auto-agent skip the ladders and pin codex / claude (the pre-0.2 defaults)
|
|
107
107
|
--jobs <n> parallel analysis calls [4]
|
|
108
108
|
|
package/src/commands/propose.js
CHANGED
|
@@ -35,6 +35,10 @@ export async function foldForRun(ctx, memoryFile) {
|
|
|
35
35
|
|
|
36
36
|
export async function runProposal(ctx, precomputed = null) {
|
|
37
37
|
const { repo, config } = ctx;
|
|
38
|
+
// Starting a new proposal run invalidates the previous result immediately. Discovery,
|
|
39
|
+
// folding, and agent resolution can all fail before synthesis starts; none of those
|
|
40
|
+
// failures may leave an older proposal available to apply as if it came from this run.
|
|
41
|
+
config.state.clearProposal();
|
|
38
42
|
const { file } = precomputed || primaryMemoryFile(repo, config);
|
|
39
43
|
const transcripts = precomputed?.transcripts || (await discoverForRun(ctx)).transcripts;
|
|
40
44
|
|
|
@@ -113,6 +117,60 @@ export function printProposal(proposal, { applied = false, analysisUsage = [] }
|
|
|
113
117
|
out("Review and apply with `backpass apply` (nothing has been written).");
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/** True when a violation is about the always-loaded budget rather than the annotation. */
|
|
121
|
+
const isBudgetViolation = (v) => /-token budget/.test(v);
|
|
122
|
+
const isEditCapViolation = (v) => /per-run cap is \d+/.test(v);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What to actually try next, read off the condition the run ended on.
|
|
126
|
+
*
|
|
127
|
+
* The old advice - a stronger model, a bigger budget, a higher edit cap - was printed for
|
|
128
|
+
* every failure, including the ones where the model never spoke and the ones where the
|
|
129
|
+
* budget was never the constraint. Each terminal condition has a different repair.
|
|
130
|
+
*/
|
|
131
|
+
export function synthesisFailureHint(err) {
|
|
132
|
+
if (err.reason === "empty") {
|
|
133
|
+
return "the synthesis harness returned no text, so nothing about the model, the budget, or the edit cap was the constraint; run `backpass propose` again to start a fresh synthesis session";
|
|
134
|
+
}
|
|
135
|
+
if (err.reason === "unparseable") {
|
|
136
|
+
return "the model answered but not with a JSON object; run `backpass propose` again, or pin a different harness with --synthesis-agent";
|
|
137
|
+
}
|
|
138
|
+
if (err.reason === "editing") {
|
|
139
|
+
return "the agent kept rewriting the staging copy instead of describing it; run `backpass propose` again to start fresh";
|
|
140
|
+
}
|
|
141
|
+
const violations = err.violations || [];
|
|
142
|
+
if (violations.some(isBudgetViolation)) {
|
|
143
|
+
return "the edit set did not clear the budget gate: raise --budget, or let the shrink continue over more runs";
|
|
144
|
+
}
|
|
145
|
+
if (violations.some(isEditCapViolation)) {
|
|
146
|
+
return "the annotation proposed more edits than the per-run learning rate allows: raise --max-edits, or re-run and let the next pass take the rest";
|
|
147
|
+
}
|
|
148
|
+
return "the gates above are what the next synthesis must satisfy; run `backpass propose` again";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Report a synthesis that ended without a valid proposal: loudly, and about the turn that
|
|
153
|
+
* actually ended it (design section 6).
|
|
154
|
+
*
|
|
155
|
+
* The saved proposal and the terminal condition can be from different turns - a run whose
|
|
156
|
+
* last turn was empty leaves the rejected proposal of an earlier one on disk - so the
|
|
157
|
+
* provenance is printed rather than letting the older violations read as this turn's.
|
|
158
|
+
*/
|
|
159
|
+
export function printSynthesisFailure(err, state) {
|
|
160
|
+
info("");
|
|
161
|
+
for (const violation of err.violations) info(` ${color.red("x")} ${violation}`);
|
|
162
|
+
info("");
|
|
163
|
+
if (!err.saved) {
|
|
164
|
+
info(color.dim(" no proposal was saved: no annotation turn produced one"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
info(color.dim(` the rejected proposal was saved to ${state.proposalPath}`));
|
|
168
|
+
if (err.reason !== "gates") {
|
|
169
|
+
info(color.dim(` it is from annotation attempt ${err.saved.attempt}, not the turn above, and it lists:`));
|
|
170
|
+
for (const violation of err.saved.violations) info(color.dim(` - ${violation}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
116
174
|
export async function cmdPropose(ctx) {
|
|
117
175
|
try {
|
|
118
176
|
const { proposal } = await runProposal(ctx);
|
|
@@ -124,12 +182,8 @@ export async function cmdPropose(ctx) {
|
|
|
124
182
|
return 0;
|
|
125
183
|
} catch (err) {
|
|
126
184
|
if (err instanceof ProposalViolation) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (const violation of err.violations) info(` ${color.red("x")} ${violation}`);
|
|
130
|
-
info("");
|
|
131
|
-
info(color.dim(` the rejected proposal was saved to ${ctx.config.state.proposalPath}`));
|
|
132
|
-
throw new UserError(err.message, "try a stronger synthesis model, or raise --budget / --max-edits");
|
|
185
|
+
printSynthesisFailure(err, ctx.config.state);
|
|
186
|
+
throw new UserError(err.message, synthesisFailureHint(err));
|
|
133
187
|
}
|
|
134
188
|
throw err;
|
|
135
189
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { UserError, color, info, json, out } from "../logger.js";
|
|
|
2
2
|
import { ProposalViolation } from "../proposal.js";
|
|
3
3
|
import { runAnalysis } from "./analyze.js";
|
|
4
4
|
import { bootstrapJson, bootstrapRun, printBootstrap } from "./bootstrap.js";
|
|
5
|
-
import { printProposal, runProposal } from "./propose.js";
|
|
5
|
+
import { printProposal, printSynthesisFailure, runProposal, synthesisFailureHint } from "./propose.js";
|
|
6
6
|
import { budgetBar, formatTokens } from "../tokens.js";
|
|
7
7
|
import { startTui } from "../tui/index.js";
|
|
8
8
|
import { resolveMemoryFiles } from "../memory.js";
|
|
@@ -10,7 +10,7 @@ import { resolveMemoryFiles } from "../memory.js";
|
|
|
10
10
|
/**
|
|
11
11
|
* The default command: one full backward pass.
|
|
12
12
|
*
|
|
13
|
-
* discover -> distill -> analyze (cheap, fanned out) -> fold -> synthesize (
|
|
13
|
+
* discover -> distill -> analyze (cheap, fanned out) -> fold -> synthesize (high-reasoning turns)
|
|
14
14
|
*
|
|
15
15
|
* It never writes - with one exception: a repo with no memory file at all is
|
|
16
16
|
* bootstrapped (`./bootstrap.js`), which only ever creates files. Otherwise applying
|
|
@@ -31,6 +31,8 @@ export async function cmdRun(ctx) {
|
|
|
31
31
|
)}`,
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
config.state.clearProposal();
|
|
35
|
+
|
|
34
36
|
if (!resolveMemoryFiles(repo.root, config.memoryFiles).primary) {
|
|
35
37
|
const result = await bootstrapRun(ctx);
|
|
36
38
|
tui?.stop();
|
|
@@ -82,11 +84,8 @@ export async function cmdRun(ctx) {
|
|
|
82
84
|
} catch (err) {
|
|
83
85
|
tui?.stop();
|
|
84
86
|
if (err instanceof ProposalViolation) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
info("");
|
|
88
|
-
info(color.dim(` the rejected proposal was saved to ${config.state.proposalPath}`));
|
|
89
|
-
throw new UserError(err.message, "try a stronger synthesis model, or raise --budget / --max-edits");
|
|
87
|
+
printSynthesisFailure(err, config.state);
|
|
88
|
+
throw new UserError(err.message, synthesisFailureHint(err));
|
|
90
89
|
}
|
|
91
90
|
throw err;
|
|
92
91
|
} finally {
|