backpass 0.1.6 → 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 +52 -11
- 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 +327 -17
- package/src/cli.js +3 -3
- package/src/commands/apply.js +2 -2
- package/src/commands/propose.js +60 -6
- package/src/commands/run.js +6 -7
- package/src/harness-invoke.js +277 -0
- package/src/memory.js +10 -1
- 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/tokens.js +11 -4
- package/src/tui/index.js +12 -0
- package/src/tui/render.js +16 -0
- package/templates/apply.html +26 -16
package/README.md
CHANGED
|
@@ -166,9 +166,9 @@ a sighting retires once the memory file gains an instruction that covers it, and
|
|
|
166
166
|
sightings expire after `gapLedgerMaxAge` (default 90d). Until a gap corroborates it stays
|
|
167
167
|
out of the proposal entirely.
|
|
168
168
|
|
|
169
|
-
### 5. Gradient descent -
|
|
169
|
+
### 5. Gradient descent - native edits
|
|
170
170
|
|
|
171
|
-
A
|
|
171
|
+
A high-reasoning synthesis run turns the aggregated gradients into concrete edits: ADD,
|
|
172
172
|
REMOVE, REWRITE, or EXTRACT→SKILL. The agent does not describe edits for backpass to
|
|
173
173
|
splice in - it makes them, with its harness's own file tools, in a **staging copy** of the
|
|
174
174
|
memory file under `.backpass/synthesis/` (the repo itself is read-only to it, for
|
|
@@ -189,10 +189,31 @@ Then mechanical gates run, and they are not negotiable:
|
|
|
189
189
|
- every edit carries a verbatim quote
|
|
190
190
|
- the post-edit file must fit the budget, measured on the staged file
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
An extraction is the created `SKILL.md` plus the memory-file change that pays for it.
|
|
193
|
+
Neighbouring removals are merged into one measured change, and a merged change cannot be
|
|
194
|
+
accepted in halves - so when several sections leave together, their skills arrive as one
|
|
195
|
+
extract with several skills, which is one honest accept/reject decision. Skills whose
|
|
196
|
+
removals were measured separately stay separate decisions, and bundling them is refused.
|
|
197
|
+
|
|
198
|
+
A malformed answer or gate violation triggers a re-prompt naming the exact breach (at
|
|
199
|
+
most two). If those also fail, backpass **fails loudly** and preserves the latest parseable
|
|
200
|
+
rejected proposal, if one was produced. It never silently truncates. A harness that writes
|
|
201
|
+
past the staging copy into the repo is an error, never an apply.
|
|
202
|
+
|
|
203
|
+
Not every annotate turn is an answer, and the three cases are kept apart because they call
|
|
204
|
+
for different things:
|
|
205
|
+
|
|
206
|
+
- **the agent edited the copy again** - the ids it was given no longer describe the files.
|
|
207
|
+
It is shown the fresh measurement and answers again; this costs no re-prompt.
|
|
208
|
+
- **the turn came back empty** - the harness returned success with no text at all. Nothing
|
|
209
|
+
was said, so there is nothing to correct: the annotation is retried once in a **new**
|
|
210
|
+
session, since the accumulated context of the old one is the likeliest cause.
|
|
211
|
+
- **the turn returned text** - malformed JSON or a gate violation uses an annotation
|
|
212
|
+
attempt and can trigger a re-prompt. Only a parseable, gate-rejected answer writes a
|
|
213
|
+
rejected proposal, stamped with the attempt that produced it.
|
|
214
|
+
|
|
215
|
+
When a run does fail, the advice it prints comes from the condition it ended on. Run
|
|
216
|
+
`backpass propose` again to start a fresh synthesis.
|
|
196
217
|
|
|
197
218
|
Token deltas shown to you are measured by backpass from the actual text - never taken from
|
|
198
219
|
the model's own arithmetic.
|
|
@@ -249,7 +270,26 @@ edit is not proposed again unless materially new evidence arrives.
|
|
|
249
270
|
The live budget gauge is not just a readout. Apply rechecks the accepted subset against
|
|
250
271
|
the same budget gate as synthesis: stay under the cap, or shrink if the file is already
|
|
251
272
|
over. An incompatible set writes nothing and does not record rejections, so you can pick
|
|
252
|
-
a compatible set and try again.
|
|
273
|
+
a compatible set and try again. If the run shrinks the file but leaves it above the cap,
|
|
274
|
+
that is progress, not a failure: it is written, and the remaining overage is printed.
|
|
275
|
+
|
|
276
|
+
Apply preflights every accepted edit before writing. The proposal was measured against one
|
|
277
|
+
exact version of your memory file, so apply first checks the file still exists and is still
|
|
278
|
+
that version. If it was removed or changed since - you pulled, edited it by hand, or another
|
|
279
|
+
agent did - the edits no longer describe what is on disk, so nothing is written and you are
|
|
280
|
+
told to run `backpass` again to re-propose against the current file. Within a run every file
|
|
281
|
+
is composed from one version: it takes every accepted edit or none of them. Apply also
|
|
282
|
+
refuses the whole write if any created skill target already exists or two accepted paths
|
|
283
|
+
resolve to the same file.
|
|
284
|
+
|
|
285
|
+
Skills and non-memory files are written only after every edit has composed, with the memory
|
|
286
|
+
file committed last. A later write failure rolls back files, skills, and loading-layout
|
|
287
|
+
entries created earlier in that round. If another process changes a committed file before
|
|
288
|
+
rollback reaches it, apply leaves that change untouched and reports the rollback conflict.
|
|
289
|
+
|
|
290
|
+
For compatibility, proposals created by older backpass versions that do not contain a
|
|
291
|
+
memory-file hash skip the freshness check. Regenerate such a proposal before applying it if
|
|
292
|
+
the repository may have changed.
|
|
253
293
|
|
|
254
294
|
```sh
|
|
255
295
|
backpass apply --no-ui # same decision, in the terminal
|
|
@@ -335,9 +375,10 @@ backpass \
|
|
|
335
375
|
--synthesis-agent claude --synthesis-model claude-opus-5 --synthesis-effort high
|
|
336
376
|
```
|
|
337
377
|
|
|
338
|
-
`--no-auto-agent` pins the pre-ladder defaults (codex / claude).
|
|
339
|
-
|
|
340
|
-
|
|
378
|
+
`--no-auto-agent` pins the pre-ladder defaults (codex / claude). Model and effort
|
|
379
|
+
overrides are scoped to that Backpass invocation, so they never rewrite your harness
|
|
380
|
+
defaults. If an adapter has no proven overlay, backpass stops rather than pretending it
|
|
381
|
+
applied, except OpenCode effort which is skipped with a report note.
|
|
341
382
|
|
|
342
383
|
### Configuration
|
|
343
384
|
|
|
@@ -388,7 +429,7 @@ Everything mutable lives in `.backpass/`, kept out of git via the repo's local e
|
|
|
388
429
|
scan-cache.json collect-samples verdicts by path + mtime + size
|
|
389
430
|
evidence/<id>.json per-transcript loss
|
|
390
431
|
evidence-summary.json aggregated gradients
|
|
391
|
-
proposal.json the latest gradient-descent step
|
|
432
|
+
proposal.json the latest parseable gradient-descent step (absent if none was produced)
|
|
392
433
|
synthesis/ the staging copy the gradient-descent agent edited (memory file + skills)
|
|
393
434
|
prompts/ the exact prompts of the last run
|
|
394
435
|
agent-probe-cache.json which harnesses were available and logged in, and when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"description": "Gradient descent for your agent memory - analyzes past agent session transcripts and proposes evidence-backed edits to AGENTS.md / CLAUDE.md",
|
|
6
6
|
"type": "module",
|
package/src/acpx.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { warn } from "./logger.js";
|
|
3
|
+
import { UserError, warn } from "./logger.js";
|
|
4
4
|
import { runCapture } from "./subprocess.js";
|
|
5
|
+
import { prepareHarnessInvocation } from "./harness-invoke.js";
|
|
5
6
|
import * as piStore from "./discovery/adapters/pi.js";
|
|
6
7
|
import { readJsonl } from "./discovery/adapters/shared.js";
|
|
7
8
|
|
|
@@ -32,13 +33,13 @@ export function acpxAgentName(agent) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* The session config-option id each adapter uses for reasoning effort
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* effort is
|
|
36
|
+
* The session config-option id each adapter uses for reasoning effort when that
|
|
37
|
+
* `set` is session-local. Pi is absent: ACP `set thought_level` rewrites
|
|
38
|
+
* `~/.pi/agent/settings.json`, so Pi effort is process `--thinking` instead
|
|
39
|
+
* (`src/harness-invoke.js`). Grok uses process `--reasoning-effort`. OpenCode
|
|
40
|
+
* has no overlay; effort is skipped with a report note, never silently.
|
|
40
41
|
*/
|
|
41
|
-
export const EFFORT_OPTION_KEYS = { codex: "reasoning_effort", claude: "effort"
|
|
42
|
+
export const EFFORT_OPTION_KEYS = { codex: "reasoning_effort", claude: "effort" };
|
|
42
43
|
|
|
43
44
|
export function effortOptionKey(agent) {
|
|
44
45
|
return EFFORT_OPTION_KEYS[agent] || null;
|
|
@@ -60,7 +61,7 @@ export class AcpxError extends Error {
|
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* @param {string[]} args
|
|
63
|
-
* @param {{ timeoutMs?: number, cwd?: string, input?: string }} [options]
|
|
64
|
+
* @param {{ timeoutMs?: number, cwd?: string, input?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
64
65
|
*/
|
|
65
66
|
function run(args, options = {}) {
|
|
66
67
|
return runCapture(ACPX_BIN, args, options);
|
|
@@ -170,6 +171,52 @@ function baseArgs({ cwd, model, timeoutSeconds, approveReads, approveAll = false
|
|
|
170
171
|
return args;
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
function invocationAgentArgs(invocation, agent) {
|
|
175
|
+
return invocation.acpxAgentCommand ? ["--agent", invocation.acpxAgentCommand] : [acpxAgentName(agent)];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function verifyHarnessInvocation(invocation, cwd) {
|
|
179
|
+
if (!invocation.requiredBuiltinAgent) return;
|
|
180
|
+
const args = [...(cwd ? ["--cwd", cwd] : []), "config", "show", "--format", "json"];
|
|
181
|
+
const result = await run(args, { timeoutMs: 10_000, cwd, env: invocation.env });
|
|
182
|
+
if (result.code !== 0) {
|
|
183
|
+
throw new UserError(
|
|
184
|
+
`cannot verify acpx adapter configuration for ${invocation.requiredBuiltinAgent}: ${firstLine(result.stderr) || `exit ${result.code}`}`,
|
|
185
|
+
"upgrade acpx or omit the model and effort override",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let config;
|
|
190
|
+
try {
|
|
191
|
+
config = JSON.parse(result.stdout);
|
|
192
|
+
} catch {
|
|
193
|
+
throw new UserError(
|
|
194
|
+
`cannot verify acpx adapter configuration for ${invocation.requiredBuiltinAgent}: config show returned invalid JSON`,
|
|
195
|
+
"upgrade acpx or omit the model and effort override",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
!config ||
|
|
201
|
+
typeof config !== "object" ||
|
|
202
|
+
Array.isArray(config) ||
|
|
203
|
+
!config.agents ||
|
|
204
|
+
typeof config.agents !== "object"
|
|
205
|
+
) {
|
|
206
|
+
throw new UserError(
|
|
207
|
+
`cannot verify acpx adapter configuration for ${invocation.requiredBuiltinAgent}: config show omitted the resolved agents map`,
|
|
208
|
+
"upgrade acpx or omit the model and effort override",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Object.prototype.hasOwnProperty.call(config.agents, invocation.requiredBuiltinAgent)) {
|
|
213
|
+
throw new UserError(
|
|
214
|
+
`cannot safely apply model or effort overrides because acpx agents.${invocation.requiredBuiltinAgent} replaces the proven built-in adapter`,
|
|
215
|
+
`remove the agents.${invocation.requiredBuiltinAgent} replacement or omit the model and effort override`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
173
220
|
/** `acpx --version`, used to key the probe cache. Null when acpx is missing. */
|
|
174
221
|
export async function acpxVersion({ timeoutMs = 10_000 } = {}) {
|
|
175
222
|
const result = await run(["--version"], { timeoutMs });
|
|
@@ -230,6 +277,7 @@ export async function probeSession({ agent, sessionName, cwd = undefined, timeou
|
|
|
230
277
|
export async function execOneShot({
|
|
231
278
|
agent,
|
|
232
279
|
model = null,
|
|
280
|
+
effort = null,
|
|
233
281
|
promptFile,
|
|
234
282
|
cwd,
|
|
235
283
|
timeoutSeconds = 300,
|
|
@@ -237,42 +285,56 @@ export async function execOneShot({
|
|
|
237
285
|
approveReads = true,
|
|
238
286
|
suppressReads = true,
|
|
239
287
|
}) {
|
|
288
|
+
const invocation = prepareHarnessInvocation({ agent, model, effort });
|
|
240
289
|
const args = [
|
|
241
|
-
...baseArgs({ cwd, model, timeoutSeconds, approveReads, suppressReads }),
|
|
290
|
+
...baseArgs({ cwd, model: invocation.acpxModel, timeoutSeconds, approveReads, suppressReads }),
|
|
242
291
|
"--prompt-retries",
|
|
243
292
|
String(promptRetries),
|
|
244
|
-
|
|
293
|
+
...invocationAgentArgs(invocation, agent),
|
|
245
294
|
"exec",
|
|
246
295
|
"--file",
|
|
247
296
|
promptFile,
|
|
248
297
|
];
|
|
249
298
|
|
|
250
299
|
const startedAt = Date.now();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
300
|
+
try {
|
|
301
|
+
await verifyHarnessInvocation(invocation, cwd);
|
|
302
|
+
if (effort && invocation.setEffortKey) {
|
|
303
|
+
throw new UserError(
|
|
304
|
+
`${agent} cannot apply invocation-scoped effort=${effort} in an exec one-shot`,
|
|
305
|
+
"use a named session or omit the effort override",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const result = await run(args, { timeoutMs: (timeoutSeconds + 30) * 1000, cwd, env: invocation.env });
|
|
309
|
+
if (result.spawnError && result.spawnError.code === "ENOENT") throw notFoundError(result);
|
|
310
|
+
if (result.timedOut) {
|
|
311
|
+
throw new AcpxError(`acpx ${agent} exec timed out after ${timeoutSeconds}s`, result);
|
|
312
|
+
}
|
|
313
|
+
if (result.code !== 0) {
|
|
314
|
+
throw new AcpxError(`acpx ${agent} exec failed (exit ${result.code})`, result);
|
|
315
|
+
}
|
|
259
316
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
317
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
318
|
+
const usage = parseTokenLine(combined) ?? recoverUsageFromStore({ agent, promptFile, cwd, startedAt });
|
|
319
|
+
return { text: stripAcpxNoise(result.stdout), usage, raw: result.stdout, notes: invocation.notes };
|
|
320
|
+
} finally {
|
|
321
|
+
invocation.dispose();
|
|
322
|
+
}
|
|
263
323
|
}
|
|
264
324
|
|
|
265
325
|
/**
|
|
266
326
|
* A named session that stays open across turns (design section 5).
|
|
267
327
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
328
|
+
* Model and effort are applied as invocation-scoped overlays (`src/harness-invoke.js`),
|
|
329
|
+
* never by rewriting persistent harness defaults. `exec` cannot carry process-level
|
|
330
|
+
* effort for every harness, so an effortful call still goes through a session when
|
|
331
|
+
* the overlay needs one. Synthesis also needs more than one turn in the same context:
|
|
332
|
+
* the agent edits the staging copy, then annotates the changes backpass measured.
|
|
333
|
+
* OpenCode alone skips its unsupported effort overlay with a report line. Every other
|
|
334
|
+
* requested overlay without a proven mechanism stops rather than pretending it applied.
|
|
273
335
|
*
|
|
274
336
|
* Resolves to the handle, or throws an `AcpxError` (`unsupported: true` when the adapter
|
|
275
|
-
* has no session support
|
|
337
|
+
* has no session support; `sessionPrompt` only falls back when it can preserve the requested overlays).
|
|
276
338
|
*
|
|
277
339
|
* @returns {Promise<{ notes: string[],
|
|
278
340
|
* prompt: (options: { promptFile: string, timeoutSeconds?: number, promptRetries?: number,
|
|
@@ -281,11 +343,33 @@ export async function execOneShot({
|
|
|
281
343
|
* close: () => Promise<void> }>}
|
|
282
344
|
*/
|
|
283
345
|
export async function openSession({ agent, model = null, effort = null, sessionName, cwd }) {
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
|
|
346
|
+
const invocation = prepareHarnessInvocation({ agent, model, effort });
|
|
347
|
+
const notes = [...invocation.notes];
|
|
348
|
+
const acpxAgentArgs = invocationAgentArgs(invocation, agent);
|
|
349
|
+
const runOpts = { timeoutMs: 60_000, cwd, env: invocation.env };
|
|
350
|
+
try {
|
|
351
|
+
await verifyHarnessInvocation(invocation, cwd);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
invocation.dispose();
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
const created = await run(
|
|
357
|
+
[
|
|
358
|
+
...(invocation.acpxModel ? ["--model", invocation.acpxModel] : []),
|
|
359
|
+
...acpxAgentArgs,
|
|
360
|
+
"sessions",
|
|
361
|
+
"new",
|
|
362
|
+
"--name",
|
|
363
|
+
sessionName,
|
|
364
|
+
],
|
|
365
|
+
runOpts,
|
|
366
|
+
);
|
|
367
|
+
if (created.spawnError && created.spawnError.code === "ENOENT") {
|
|
368
|
+
invocation.dispose();
|
|
369
|
+
throw notFoundError(created);
|
|
370
|
+
}
|
|
288
371
|
if (created.code !== 0) {
|
|
372
|
+
invocation.dispose();
|
|
289
373
|
// An auth or spawn failure must surface as such so the caller can fall through.
|
|
290
374
|
if (classifyAcpxFailure(created)) {
|
|
291
375
|
throw new AcpxError(`acpx ${agent} session create failed: ${firstLine(created.stderr)}`, created);
|
|
@@ -306,27 +390,23 @@ export async function openSession({ agent, model = null, effort = null, sessionN
|
|
|
306
390
|
const close = async () => {
|
|
307
391
|
if (closed) return;
|
|
308
392
|
closed = true;
|
|
309
|
-
const result = await run([
|
|
393
|
+
const result = await run([...acpxAgentArgs, "sessions", "close", sessionName], {
|
|
394
|
+
timeoutMs: 30_000,
|
|
395
|
+
cwd,
|
|
396
|
+
env: invocation.env,
|
|
397
|
+
});
|
|
310
398
|
if (result.code !== 0) warn(`could not close acpx session ${sessionName}`);
|
|
399
|
+
invocation.dispose();
|
|
311
400
|
};
|
|
312
401
|
|
|
313
402
|
try {
|
|
314
|
-
if (
|
|
315
|
-
const set = await run([
|
|
403
|
+
if (effort && invocation.setEffortKey) {
|
|
404
|
+
const set = await run([...acpxAgentArgs, "-s", sessionName, "set", invocation.setEffortKey, effort], runOpts);
|
|
316
405
|
if (set.code !== 0) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (effort) {
|
|
324
|
-
const key = effortOptionKey(agent);
|
|
325
|
-
const set = key
|
|
326
|
-
? await run([acpxAgent, "-s", sessionName, "set", key, effort], { timeoutMs: 60_000, cwd })
|
|
327
|
-
: null;
|
|
328
|
-
if (!set || set.code !== 0) {
|
|
329
|
-
notes.push(`${agent} does not advertise a reasoning-effort option; ran without effort=${effort}`);
|
|
406
|
+
throw new AcpxError(
|
|
407
|
+
`acpx ${agent} could not apply invocation-scoped effort=${effort}: ${firstLine(set.stderr) || `exit ${set.code}`}`,
|
|
408
|
+
set,
|
|
409
|
+
);
|
|
330
410
|
}
|
|
331
411
|
}
|
|
332
412
|
} catch (err) {
|
|
@@ -348,13 +428,13 @@ export async function openSession({ agent, model = null, effort = null, sessionN
|
|
|
348
428
|
...baseArgs({ cwd, model: null, timeoutSeconds, approveReads, approveAll, suppressReads }),
|
|
349
429
|
"--prompt-retries",
|
|
350
430
|
String(promptRetries),
|
|
351
|
-
|
|
431
|
+
...acpxAgentArgs,
|
|
352
432
|
"-s",
|
|
353
433
|
sessionName,
|
|
354
434
|
"--file",
|
|
355
435
|
promptFile,
|
|
356
436
|
];
|
|
357
|
-
const result = await run(args, { timeoutMs: (timeoutSeconds + 30) * 1000, cwd });
|
|
437
|
+
const result = await run(args, { timeoutMs: (timeoutSeconds + 30) * 1000, cwd, env: invocation.env });
|
|
358
438
|
if (result.timedOut) throw new AcpxError(`acpx ${agent} session prompt timed out after ${timeoutSeconds}s`, result);
|
|
359
439
|
if (result.code !== 0) throw new AcpxError(`acpx ${agent} session prompt failed (exit ${result.code})`, result);
|
|
360
440
|
|
|
@@ -384,8 +464,9 @@ function subtractUsage(current, previous) {
|
|
|
384
464
|
|
|
385
465
|
/**
|
|
386
466
|
* One prompt through a short-lived named session: open, prompt, close. The analysis
|
|
387
|
-
* pass uses this whenever an effort is configured.
|
|
388
|
-
*
|
|
467
|
+
* pass uses this whenever an effort is configured. Without session support, it falls
|
|
468
|
+
* back to one-shot `exec` only when that preserves the requested overlays; otherwise it
|
|
469
|
+
* stops with actionable guidance. OpenCode effort remains the explicit reported skip.
|
|
389
470
|
*/
|
|
390
471
|
export async function sessionPrompt({
|
|
391
472
|
agent,
|
|
@@ -404,11 +485,20 @@ export async function sessionPrompt({
|
|
|
404
485
|
session = await openSession({ agent, model, effort, sessionName, cwd });
|
|
405
486
|
} catch (err) {
|
|
406
487
|
if (!(err instanceof AcpxError) || !err.unsupported) throw err;
|
|
488
|
+
if (effort && (agent === "claude" || agent === "codex")) {
|
|
489
|
+
throw new UserError(
|
|
490
|
+
`${agent} cannot apply invocation-scoped effort=${effort} because its acpx adapter does not support sessions`,
|
|
491
|
+
"upgrade acpx or omit the effort override",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
407
494
|
const notes = [`session unsupported for ${agent}; fell back to exec one-shot`];
|
|
408
|
-
if (effort
|
|
495
|
+
if (effort && agent === "opencode") {
|
|
496
|
+
notes.push(`${agent} does not advertise a reasoning-effort option; ran without effort=${effort}`);
|
|
497
|
+
}
|
|
409
498
|
const fallback = await execOneShot({
|
|
410
499
|
agent,
|
|
411
500
|
model,
|
|
501
|
+
effort: agent === "opencode" ? null : effort,
|
|
412
502
|
promptFile,
|
|
413
503
|
cwd,
|
|
414
504
|
timeoutSeconds,
|
package/src/analyze.js
CHANGED
|
@@ -8,7 +8,7 @@ import { renderInstructionIndex } from "./memory.js";
|
|
|
8
8
|
import { renderPrompt } from "./prompts.js";
|
|
9
9
|
import { evidenceKey, isEvidenceFresh, safeFileName } from "./state.js";
|
|
10
10
|
import { emitProgress } from "./progress.js";
|
|
11
|
-
import { color, info, warn } from "./logger.js";
|
|
11
|
+
import { UserError, color, info, warn } from "./logger.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Stage 1 of the pipeline (design section 3): one cheap model call per transcript,
|
|
@@ -137,8 +137,8 @@ async function analyzeOne({ transcript, memoryFile, config, repo, slot = 0 }) {
|
|
|
137
137
|
timeoutSeconds: config.timeoutSeconds,
|
|
138
138
|
promptRetries: config.promptRetries,
|
|
139
139
|
};
|
|
140
|
-
//
|
|
141
|
-
//
|
|
140
|
+
// Route effortful calls through a fresh per-transcript session so each harness's
|
|
141
|
+
// invocation-scoped overlay or safe fallback is applied; otherwise one-shot is cheaper.
|
|
142
142
|
if (!pick.effort) return execOneShot(call);
|
|
143
143
|
callCounter += 1;
|
|
144
144
|
return sessionPrompt({
|
|
@@ -265,6 +265,7 @@ export async function analyzeTranscripts({ transcripts, memoryFile, config, repo
|
|
|
265
265
|
emitProgress("analyze:evidence", { ...evidenceTotals });
|
|
266
266
|
}
|
|
267
267
|
} catch (err) {
|
|
268
|
+
if (err instanceof UserError) throw err;
|
|
268
269
|
// Per-transcript fail-soft: recorded, listed by `backpass status`, retried next run.
|
|
269
270
|
summary.failed += 1;
|
|
270
271
|
warn(`${transcript.harness} ${transcriptLabel(transcript)}: ${err.message}`);
|
package/src/apply/terminal.js
CHANGED
|
@@ -2,6 +2,7 @@ import readline from "node:readline/promises";
|
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
|
|
4
4
|
import { UserError, color } from "../logger.js";
|
|
5
|
+
import { editSkills } from "../skills.js";
|
|
5
6
|
import { formatTokens } from "../tokens.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -51,10 +52,10 @@ export function renderEdit(edit, index, total) {
|
|
|
51
52
|
out.push("");
|
|
52
53
|
out.push(renderDiff(edit));
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
for (const skill of edit.kind === "extract" ? editSkills(edit) : []) {
|
|
55
56
|
out.push("");
|
|
56
|
-
out.push(color.dim(` new skill: ${
|
|
57
|
-
out.push(color.dim(` description: ${
|
|
57
|
+
out.push(color.dim(` new skill: ${skill.path}`));
|
|
58
|
+
out.push(color.dim(` description: ${skill.description}`));
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
if (edit.evidence?.length) {
|