backpass 0.1.2 → 0.1.4
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 +14 -9
- package/package.json +1 -1
- package/src/apply/writer.js +54 -4
- package/src/commands/apply.js +3 -2
- package/src/discovery/adapters/hermes.js +21 -12
- package/src/proposal.js +6 -4
- package/src/tokens.js +7 -0
package/README.md
CHANGED
|
@@ -80,15 +80,15 @@ backpass apply # review each edit, accept or reject, then write
|
|
|
80
80
|
|
|
81
81
|
backpass reads the local transcript stores of seven harnesses directly. No API, no upload.
|
|
82
82
|
|
|
83
|
-
| Harness | Store | Repo tie
|
|
84
|
-
| -------------- | ---------------------------------------------- |
|
|
85
|
-
| **claude** | `~/.claude/projects/<munged-cwd>/<uuid>.jsonl` | per-line `cwd`
|
|
86
|
-
| **codex** | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | `cwd` + recorded `git.repository_url`
|
|
87
|
-
| **pi** | `~/.pi/agent/sessions/<escaped-cwd>/*.jsonl` | session-header `cwd`
|
|
88
|
-
| **opencode** | `~/.local/share/opencode/opencode.db` (sqlite) | `session.directory`
|
|
89
|
-
| **grok** | `~/.grok/sessions/<encoded-cwd>/<uuid>/` | `summary.json` `cwd` + `git_remotes`
|
|
90
|
-
| **cursor CLI** | `~/.cursor/chats/<md5(cwd)>/<uuid>/` | `meta.json` `cwd`
|
|
91
|
-
| **hermes** | `~/.hermes/state.db` (sqlite) | CLI prompt
|
|
83
|
+
| Harness | Store | Repo tie |
|
|
84
|
+
| -------------- | ---------------------------------------------- | --------------------------------------------------- |
|
|
85
|
+
| **claude** | `~/.claude/projects/<munged-cwd>/<uuid>.jsonl` | per-line `cwd` |
|
|
86
|
+
| **codex** | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | `cwd` + recorded `git.repository_url` |
|
|
87
|
+
| **pi** | `~/.pi/agent/sessions/<escaped-cwd>/*.jsonl` | session-header `cwd` |
|
|
88
|
+
| **opencode** | `~/.local/share/opencode/opencode.db` (sqlite) | `session.directory` |
|
|
89
|
+
| **grok** | `~/.grok/sessions/<encoded-cwd>/<uuid>/` | `summary.json` `cwd` + `git_remotes` |
|
|
90
|
+
| **cursor CLI** | `~/.cursor/chats/<md5(cwd)>/<uuid>/` | `meta.json` `cwd` |
|
|
91
|
+
| **hermes** | `~/.hermes/state.db` (sqlite) | session cwd, with CLI prompt / ACP config fallbacks |
|
|
92
92
|
|
|
93
93
|
Hermes collection includes CLI and ACP sessions only. Gateway, cron, and WhatsApp sessions
|
|
94
94
|
are excluded because their recorded cwd belongs to the shared gateway process, not a project.
|
|
@@ -241,6 +241,11 @@ a headless box or `--no-open` just hands you the link.
|
|
|
241
241
|
There is no DEFER button, and it isn't missing: **rejections are remembered.** A rejected
|
|
242
242
|
edit is not proposed again unless materially new evidence arrives.
|
|
243
243
|
|
|
244
|
+
The live budget gauge is not just a readout. Apply rechecks the accepted subset against
|
|
245
|
+
the same budget gate as synthesis: stay under the cap, or shrink if the file is already
|
|
246
|
+
over. An incompatible set writes nothing and does not record rejections, so you can pick
|
|
247
|
+
a compatible set and try again.
|
|
248
|
+
|
|
244
249
|
```sh
|
|
245
250
|
backpass apply --no-ui # same decision, in the terminal
|
|
246
251
|
backpass apply --no-open # print the surface URL, don't launch a browser
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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/apply/writer.js
CHANGED
|
@@ -1,17 +1,54 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { applyEdit } from "../proposal.js";
|
|
5
|
-
import { budgetStatus } from "../tokens.js";
|
|
4
|
+
import { applyEdit, projectWithDecisions } from "../proposal.js";
|
|
5
|
+
import { budgetGateKind, budgetStatus } from "../tokens.js";
|
|
6
6
|
import { recordRejection } from "../state.js";
|
|
7
7
|
import { writeSkill } from "../skills.js";
|
|
8
8
|
|
|
9
|
+
function acceptedSubsetBudgetFailure({ proposal, accepted, repo, capTokens }) {
|
|
10
|
+
if (!accepted.length) return null;
|
|
11
|
+
|
|
12
|
+
const relative = proposal.memoryFile.path;
|
|
13
|
+
const absolute = path.join(repo.root, relative);
|
|
14
|
+
if (!fs.existsSync(absolute)) return null;
|
|
15
|
+
|
|
16
|
+
const before = fs.readFileSync(absolute, "utf8");
|
|
17
|
+
const { budget } = projectWithDecisions(
|
|
18
|
+
before,
|
|
19
|
+
accepted,
|
|
20
|
+
accepted.map((edit) => edit.id),
|
|
21
|
+
capTokens,
|
|
22
|
+
);
|
|
23
|
+
const gate = budgetGateKind(budget);
|
|
24
|
+
if (gate === "cap") {
|
|
25
|
+
return {
|
|
26
|
+
file: relative,
|
|
27
|
+
error:
|
|
28
|
+
`accepted edits leave ${relative} at ${budget.projected} tokens, ${budget.over} over the ` +
|
|
29
|
+
`${capTokens}-token budget; choose a compatible set of edits`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (gate === "shrink") {
|
|
33
|
+
return {
|
|
34
|
+
file: relative,
|
|
35
|
+
error:
|
|
36
|
+
`${relative} is already ${budget.current - capTokens} tokens over the ${capTokens}-token budget, ` +
|
|
37
|
+
`so accepted edits must shrink it, but they change it by ${budget.delta >= 0 ? "+" : ""}${budget.delta} ` +
|
|
38
|
+
"tokens; choose a compatible set of edits",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
/**
|
|
10
45
|
* The only place in backpass that writes to the repo.
|
|
11
46
|
*
|
|
12
47
|
* Everything upstream is read-only analysis; a run only changes the weights here, after
|
|
13
|
-
* a human accepted specific edits.
|
|
14
|
-
*
|
|
48
|
+
* a human accepted specific edits. The accepted subset is rechecked with the same
|
|
49
|
+
* cap/shrink budget gate as the full proposal (`budgetGateKind`); a failing subset
|
|
50
|
+
* returns with no writes and no rejection ledger. Writes are grouped per file so a
|
|
51
|
+
* memory file is rewritten once, atomically, rather than edit by edit.
|
|
15
52
|
*/
|
|
16
53
|
export function applyDecisions({ proposal, decisions, repo, state, config, dryRun = false }) {
|
|
17
54
|
const accepted = proposal.edits.filter((e) => decisions[e.id] === "accepted");
|
|
@@ -25,8 +62,20 @@ export function applyDecisions({ proposal, decisions, repo, state, config, dryRu
|
|
|
25
62
|
warnings: [],
|
|
26
63
|
accepted: accepted.length,
|
|
27
64
|
rejected: rejected.length,
|
|
65
|
+
rejectionsRecorded: false,
|
|
28
66
|
};
|
|
29
67
|
|
|
68
|
+
const budgetFailure = acceptedSubsetBudgetFailure({
|
|
69
|
+
proposal,
|
|
70
|
+
accepted,
|
|
71
|
+
repo,
|
|
72
|
+
capTokens: config.budgetTokens,
|
|
73
|
+
});
|
|
74
|
+
if (budgetFailure) {
|
|
75
|
+
results.failed.push(budgetFailure);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
30
79
|
for (const edit of accepted) {
|
|
31
80
|
if (!byFile.has(edit.file)) byFile.set(edit.file, []);
|
|
32
81
|
byFile.get(edit.file).push(edit);
|
|
@@ -76,6 +125,7 @@ export function applyDecisions({ proposal, decisions, repo, state, config, dryRu
|
|
|
76
125
|
const rejections = state.readRejections();
|
|
77
126
|
for (const edit of rejected) recordRejection(edit, rejections);
|
|
78
127
|
state.writeRejections(rejections);
|
|
128
|
+
results.rejectionsRecorded = true;
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
return results;
|
package/src/commands/apply.js
CHANGED
|
@@ -10,7 +10,8 @@ import { budgetBar, formatTokens } from "../tokens.js";
|
|
|
10
10
|
*
|
|
11
11
|
* By default it serves the shipped static template through lavish-axi and waits for one
|
|
12
12
|
* structured decision vector; `--no-ui` keeps the same ACCEPT/REJECT decision in the
|
|
13
|
-
* terminal.
|
|
13
|
+
* terminal. `applyDecisions` revalidates the accepted subset against the budget before
|
|
14
|
+
* writing; a failing set records no rejections.
|
|
14
15
|
*/
|
|
15
16
|
export async function cmdApply(ctx) {
|
|
16
17
|
const { config, repo } = ctx;
|
|
@@ -97,7 +98,7 @@ export async function cmdApply(ctx) {
|
|
|
97
98
|
out(` ${color.red("failed")} ${failure.file}${failure.edit ? ` (${failure.edit})` : ""}: ${failure.error}`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
if (results.
|
|
101
|
+
if (results.rejectionsRecorded) {
|
|
101
102
|
out(color.dim(" rejections recorded - they will not be re-proposed without new evidence"));
|
|
102
103
|
}
|
|
103
104
|
if (!results.written.length && !results.skills.length) out(" nothing written");
|
|
@@ -7,15 +7,15 @@ import { openReadOnly, safeJsonParse } from "./sqlite.js";
|
|
|
7
7
|
* hermes: ~/.hermes/state.db (sqlite)
|
|
8
8
|
*
|
|
9
9
|
* Observed schema version 13 (upstream is 26; SELECT named columns so additive
|
|
10
|
-
* columns are tolerated).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* columns are tolerated). v26 adds sessions.cwd; CLI rows leave system_prompt
|
|
11
|
+
* NULL and store the project path there. ACP still snapshots cwd on
|
|
12
|
+
* model_config. Prefer the cwd column when present, then the v13 paths:
|
|
13
13
|
* acp - model_config.cwd when it is an absolute path
|
|
14
14
|
* cli - first `Current working directory:` / `Working directory:` line in
|
|
15
15
|
* system_prompt
|
|
16
|
-
* Gateway / cron / whatsapp sessions are skipped
|
|
17
|
-
* gateway process cwd, not a project, and would pin
|
|
18
|
-
* repo. Sessions with no recoverable cwd are skipped.
|
|
16
|
+
* Gateway / cron / whatsapp sessions are skipped even if sessions.cwd is set:
|
|
17
|
+
* their path is the gateway process cwd, not a project, and would pin
|
|
18
|
+
* unrelated sessions to one repo. Sessions with no recoverable cwd are skipped.
|
|
19
19
|
*
|
|
20
20
|
* Timestamps are epoch seconds; backpass uses milliseconds (x1000).
|
|
21
21
|
* Structured content uses a `\x00json:` prefix; node:sqlite truncates TEXT at
|
|
@@ -62,17 +62,25 @@ function cwdFromPrompt(prompt) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function recoverCwd(row, source) {
|
|
65
|
+
if (looksAbsolute(row.cwd)) return row.cwd;
|
|
65
66
|
if (source === "acp") return cwdFromConfig(row.model_config);
|
|
66
67
|
if (source === "cli") return cwdFromPrompt(row.system_prompt);
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
.prepare(
|
|
71
|
+
function tableHasColumn(db, table, column) {
|
|
72
|
+
return db
|
|
73
|
+
.prepare(`PRAGMA table_info(${table})`)
|
|
73
74
|
.all()
|
|
74
|
-
.some((
|
|
75
|
-
|
|
75
|
+
.some((entry) => entry.name === column);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function activeMessageFilter(db, alias = "") {
|
|
79
|
+
return tableHasColumn(db, "messages", "active") ? ` AND ${alias}active = 1` : "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sessionCwdSelect(db) {
|
|
83
|
+
return tableHasColumn(db, "sessions", "cwd") ? ", s.cwd" : "";
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
@@ -87,10 +95,11 @@ export async function discover({ cutoffMs } = {}) {
|
|
|
87
95
|
const cutoffSec = cutoffMs == null ? null : cutoffMs / 1000;
|
|
88
96
|
try {
|
|
89
97
|
const activeFilter = activeMessageFilter(db, "m.");
|
|
98
|
+
const cwdSelect = sessionCwdSelect(db);
|
|
90
99
|
const rows = db
|
|
91
100
|
.prepare(
|
|
92
101
|
`SELECT s.id, s.source, s.model, s.model_config, s.system_prompt, s.title,
|
|
93
|
-
s.started_at, s.ended_at,
|
|
102
|
+
s.started_at, s.ended_at${cwdSelect},
|
|
94
103
|
MAX(s.started_at,
|
|
95
104
|
COALESCE(s.ended_at, s.started_at),
|
|
96
105
|
COALESCE((SELECT MAX(m.timestamp)
|
package/src/proposal.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { renderHunkLines } from "./diff.js";
|
|
2
|
-
import { budgetStatus, estimateTokens } from "./tokens.js";
|
|
2
|
+
import { budgetGateKind, budgetStatus, estimateTokens } from "./tokens.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The proposal model: what a synthesis pass is allowed to produce, and the mechanical
|
|
6
|
-
* gates it must clear before a human ever sees it (design sections 3, 6, 7).
|
|
6
|
+
* gates it must clear before a human ever sees it (design sections 3, 6, 7). The
|
|
7
|
+
* budget gate (`budgetGateKind`) runs again on the accepted subset at apply.
|
|
7
8
|
*
|
|
8
9
|
* The synthesis agent edits a staging copy of the memory file natively
|
|
9
10
|
* (`src/workspace.js`); backpass measures the result as anchored hunks (`src/diff.js`)
|
|
@@ -355,12 +356,13 @@ export function buildProposal(rawResult, context) {
|
|
|
355
356
|
budget.mode = memoryFile.tokens > config.budgetTokens ? "shrink" : "cap";
|
|
356
357
|
budget.startedOverBudget = budget.mode === "shrink";
|
|
357
358
|
|
|
358
|
-
|
|
359
|
+
const gate = budgetGateKind(budget);
|
|
360
|
+
if (gate === "cap") {
|
|
359
361
|
violations.push(
|
|
360
362
|
`applying every proposed edit leaves ${memoryFile.path} at ${budget.projected} tokens, ` +
|
|
361
363
|
`${budget.over} over the ${config.budgetTokens}-token budget`,
|
|
362
364
|
);
|
|
363
|
-
} else if (
|
|
365
|
+
} else if (gate === "shrink") {
|
|
364
366
|
violations.push(
|
|
365
367
|
`${memoryFile.path} is already ${budget.current - config.budgetTokens} tokens over the ` +
|
|
366
368
|
`${config.budgetTokens}-token budget, so this run must shrink it, but the proposed edits ` +
|
package/src/tokens.js
CHANGED
|
@@ -40,6 +40,13 @@ export function budgetStatus(currentText, projectedText, capTokens) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Cap: stay under. Shrink: already over, so the delta must be negative. */
|
|
44
|
+
export function budgetGateKind(budget) {
|
|
45
|
+
if (budget.current <= budget.capTokens && !budget.withinBudget) return "cap";
|
|
46
|
+
if (budget.current > budget.capTokens && budget.delta >= 0) return "shrink";
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
/** Fixed-width ASCII gauge for `backpass status`. */
|
|
44
51
|
export function budgetBar(status, width = 32) {
|
|
45
52
|
const filled = Math.min(width, Math.round(status.utilization * width));
|