backpass 0.1.0 → 0.1.1
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 +8 -0
- package/package.json +1 -1
- package/src/apply/browser.js +49 -0
- package/src/apply/lavish.js +30 -5
- package/src/cli.js +2 -0
- package/src/commands/apply.js +3 -0
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
<a href="https://x.com/kunchenguid"
|
|
16
16
|
><img alt="X" src="https://img.shields.io/badge/X-@kunchenguid-black?style=flat-square"
|
|
17
17
|
/></a>
|
|
18
|
+
<a href="https://discord.gg/Wsy2NpnZDu"
|
|
19
|
+
><img
|
|
20
|
+
alt="Discord"
|
|
21
|
+
src="https://img.shields.io/discord/1439901831038763092?style=flat-square&label=discord"
|
|
22
|
+
/></a>
|
|
18
23
|
</p>
|
|
19
24
|
|
|
20
25
|
<h3 align="center">Gradient descent for your agent memory.</h3>
|
|
@@ -226,12 +231,15 @@ the evidence quotes and their sources, a live budget gauge, and ACCEPT / REJECT.
|
|
|
226
231
|
|
|
227
232
|
The surface is a static template shipped in the package - the CLI injects one JSON payload,
|
|
228
233
|
so it is instant, deterministic, and identical every run. Nothing there is model-generated.
|
|
234
|
+
It opens in your default browser when one is available; the URL is always printed too, so
|
|
235
|
+
a headless box or `--no-open` just hands you the link.
|
|
229
236
|
|
|
230
237
|
There is no DEFER button, and it isn't missing: **rejections are remembered.** A rejected
|
|
231
238
|
edit is not proposed again unless materially new evidence arrives.
|
|
232
239
|
|
|
233
240
|
```sh
|
|
234
241
|
backpass apply --no-ui # same decision, in the terminal
|
|
242
|
+
backpass apply --no-open # print the surface URL, don't launch a browser
|
|
235
243
|
backpass apply --dry-run # show what would be written
|
|
236
244
|
```
|
|
237
245
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Open a URL in the user's default browser, best effort.
|
|
5
|
+
*
|
|
6
|
+
* The review surface URL is always printed as the fallback, so this must never throw or
|
|
7
|
+
* block: a missing opener, a headless box, or a crashing helper all degrade to "print the
|
|
8
|
+
* URL only". Returns true when an opener was launched, false when the environment opted
|
|
9
|
+
* out or had no display.
|
|
10
|
+
*
|
|
11
|
+
* Dependencies are injectable so the decision logic is testable without a real browser.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {(bin: string, args: string[], options: object) => { on?: Function, unref?: Function }} Spawner
|
|
14
|
+
* @param {string | null} url
|
|
15
|
+
* @param {{ platform?: string, env?: Record<string, string | undefined>, spawnFn?: Spawner }} [deps]
|
|
16
|
+
*/
|
|
17
|
+
export function openInBrowser(url, { platform = process.platform, env = process.env, spawnFn = spawn } = {}) {
|
|
18
|
+
if (!url || !/^https?:\/\//.test(url)) return false;
|
|
19
|
+
if (!canOpenBrowser({ platform, env })) return false;
|
|
20
|
+
|
|
21
|
+
const { bin, args } = openerCommand(url, platform);
|
|
22
|
+
try {
|
|
23
|
+
const child = spawnFn(bin, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
24
|
+
// A missing or failing opener must not surface as an unhandled error.
|
|
25
|
+
child.on?.("error", () => {});
|
|
26
|
+
child.unref?.();
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Headless detection: honor explicit opt-outs, CI, and display-less Linux.
|
|
35
|
+
* @param {{ platform?: string, env?: Record<string, string | undefined> }} [deps]
|
|
36
|
+
*/
|
|
37
|
+
export function canOpenBrowser({ platform = process.platform, env = process.env } = {}) {
|
|
38
|
+
if (env.BACKPASS_NO_BROWSER || env.CI) return false;
|
|
39
|
+
if (platform === "darwin" || platform === "win32") return true;
|
|
40
|
+
return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @returns {{ bin: string, args: string[] }} */
|
|
44
|
+
function openerCommand(url, platform) {
|
|
45
|
+
if (platform === "darwin") return { bin: "open", args: [url] };
|
|
46
|
+
// `start` treats its first quoted argument as the window title; pass an empty one.
|
|
47
|
+
if (platform === "win32") return { bin: "cmd", args: ["/c", "start", "", url] };
|
|
48
|
+
return { bin: "xdg-open", args: [url] };
|
|
49
|
+
}
|
package/src/apply/lavish.js
CHANGED
|
@@ -88,18 +88,37 @@ export async function openApplySurface(file) {
|
|
|
88
88
|
if (result.code !== 0) {
|
|
89
89
|
throw new UserError(`${LAVISH_BIN} failed to open the apply surface`, result.stderr.trim().slice(0, 400));
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
return url ? url[1] : null;
|
|
91
|
+
return extractUrl(`${result.stdout}\n${result.stderr}`);
|
|
93
92
|
}
|
|
94
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Pull the session URL out of lavish-axi's output. The CLI prints it YAML-style as
|
|
96
|
+
* `url: "http://..."`, so a bare `\S+` would swallow the closing quote; stop at any
|
|
97
|
+
* quote or bracket and drop trailing punctuation.
|
|
98
|
+
*/
|
|
99
|
+
export function extractUrl(text) {
|
|
100
|
+
const match = /https?:\/\/[^\s"'<>()[\]]+/.exec(text || "");
|
|
101
|
+
if (!match) return null;
|
|
102
|
+
return match[0].replace(/[.,;:!?]+$/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Breathing room between polls that came back without a decision vector. */
|
|
106
|
+
export const POLL_RETRY_DELAY_MS = 1000;
|
|
107
|
+
|
|
95
108
|
/**
|
|
96
109
|
* Long-poll for the human's decision vector. `lavish-axi poll` blocks until the reviewer
|
|
97
110
|
* sends feedback, so this is intentionally a foreground wait.
|
|
111
|
+
*
|
|
112
|
+
* Feedback that is not a decision vector (a comment, a queued layout report) keeps the
|
|
113
|
+
* wait going. Each state is announced once: the wait line on entry, and a single note the
|
|
114
|
+
* first time non-decision feedback arrives - never one line per poll cycle, which on a
|
|
115
|
+
* chatty surface floods the terminal.
|
|
98
116
|
*/
|
|
99
|
-
export async function pollDecisions(file, editIds) {
|
|
117
|
+
export async function pollDecisions(file, editIds, { delayMs = POLL_RETRY_DELAY_MS } = {}) {
|
|
100
118
|
info(
|
|
101
119
|
`${color.dim("waiting for your decisions in the browser (Ctrl-C to abort; nothing is written until you send)")}`,
|
|
102
120
|
);
|
|
121
|
+
let notedOtherFeedback = false;
|
|
103
122
|
|
|
104
123
|
for (;;) {
|
|
105
124
|
const result = await runLavish(["poll", file]);
|
|
@@ -118,8 +137,14 @@ export async function pollDecisions(file, editIds) {
|
|
|
118
137
|
warn("review session ended without a decision vector - nothing applied");
|
|
119
138
|
return null;
|
|
120
139
|
}
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
|
|
141
|
+
if (!notedOtherFeedback) {
|
|
142
|
+
notedOtherFeedback = true;
|
|
143
|
+
info(
|
|
144
|
+
`${color.dim("feedback arrived without a decision vector - click APPLY in the browser and send from the panel; still waiting")}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
150
|
|
package/src/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ const OPTIONS = {
|
|
|
53
53
|
|
|
54
54
|
"dry-run": { type: "boolean" },
|
|
55
55
|
"no-ui": { type: "boolean" },
|
|
56
|
+
"no-open": { type: "boolean" },
|
|
56
57
|
"no-auto-agent": { type: "boolean" },
|
|
57
58
|
force: { type: "boolean" },
|
|
58
59
|
limit: { type: "string" },
|
|
@@ -114,6 +115,7 @@ BUDGET AND SHAPE
|
|
|
114
115
|
|
|
115
116
|
APPLY
|
|
116
117
|
--no-ui terminal accept/reject instead of the Lavish surface
|
|
118
|
+
--no-open print the review surface URL without opening a browser
|
|
117
119
|
--dry-run show what would be written, write nothing
|
|
118
120
|
--force re-analyze transcripts that already have fresh evidence,
|
|
119
121
|
and re-probe agents instead of trusting the probe cache
|
package/src/commands/apply.js
CHANGED
|
@@ -2,6 +2,7 @@ import { UserError, color, info, json, out, warn } from "../logger.js";
|
|
|
2
2
|
import { applyDecisions } from "../apply/writer.js";
|
|
3
3
|
import { closeApplySurface, openApplySurface, pollDecisions, renderApplySurface } from "../apply/lavish.js";
|
|
4
4
|
import { reviewInTerminal } from "../apply/terminal.js";
|
|
5
|
+
import { openInBrowser } from "../apply/browser.js";
|
|
5
6
|
import { budgetBar, formatTokens } from "../tokens.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -45,6 +46,8 @@ export async function cmdApply(ctx) {
|
|
|
45
46
|
surfaceFile = renderApplySurface(proposal, config.state, ctx.version);
|
|
46
47
|
const url = await openApplySurface(surfaceFile);
|
|
47
48
|
info(`${color.cyan("·")} review surface: ${url || surfaceFile}`);
|
|
49
|
+
// Best effort: the printed URL above is the fallback when nothing opens.
|
|
50
|
+
if (!ctx.flags["no-open"]) openInBrowser(url);
|
|
48
51
|
decisions = await pollDecisions(surfaceFile, editIds);
|
|
49
52
|
}
|
|
50
53
|
|