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 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.0",
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
+ }
@@ -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
- const url = /(https?:\/\/\S+)/.exec(`${result.stdout}\n${result.stderr}`);
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
- // Feedback that was not a decision vector (a comment, a layout report): keep waiting.
122
- info(`${color.dim("received feedback without a decision vector; still waiting")}`);
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
@@ -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