backpass 0.1.0
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/LICENSE +21 -0
- package/README.md +406 -0
- package/bin/backpass.js +4 -0
- package/package.json +62 -0
- package/src/acpx.js +576 -0
- package/src/agents.js +389 -0
- package/src/analyze.js +289 -0
- package/src/apply/lavish.js +128 -0
- package/src/apply/terminal.js +119 -0
- package/src/apply/writer.js +101 -0
- package/src/bootstrap.js +74 -0
- package/src/cli.js +261 -0
- package/src/commands/analyze.js +88 -0
- package/src/commands/apply.js +103 -0
- package/src/commands/bootstrap.js +172 -0
- package/src/commands/init.js +59 -0
- package/src/commands/propose.js +136 -0
- package/src/commands/run.js +95 -0
- package/src/commands/scan.js +90 -0
- package/src/commands/status.js +143 -0
- package/src/commands/usage.js +25 -0
- package/src/config.js +249 -0
- package/src/diff.js +305 -0
- package/src/discovery/adapters/claude.js +77 -0
- package/src/discovery/adapters/codex.js +162 -0
- package/src/discovery/adapters/cursor-cli.js +109 -0
- package/src/discovery/adapters/cursor-ide.js +130 -0
- package/src/discovery/adapters/grok.js +107 -0
- package/src/discovery/adapters/opencode.js +151 -0
- package/src/discovery/adapters/pi.js +87 -0
- package/src/discovery/adapters/shared.js +195 -0
- package/src/discovery/adapters/sqlite.js +50 -0
- package/src/discovery/association.js +100 -0
- package/src/discovery/index.js +226 -0
- package/src/discovery/self.js +62 -0
- package/src/distill.js +182 -0
- package/src/fold.js +214 -0
- package/src/gap-ledger.js +174 -0
- package/src/logger.js +74 -0
- package/src/memory.js +244 -0
- package/src/progress.js +29 -0
- package/src/prompts/analysis.md +48 -0
- package/src/prompts/annotate.md +48 -0
- package/src/prompts/synthesis.md +98 -0
- package/src/prompts.js +36 -0
- package/src/proposal.js +430 -0
- package/src/redact.js +36 -0
- package/src/repo.js +118 -0
- package/src/sample.js +99 -0
- package/src/skills.js +207 -0
- package/src/state.js +202 -0
- package/src/subprocess.js +47 -0
- package/src/synthesize.js +287 -0
- package/src/tokens.js +48 -0
- package/src/tui/index.js +336 -0
- package/src/tui/render.js +487 -0
- package/src/tui/term.js +130 -0
- package/src/tui/theme.js +111 -0
- package/src/workspace.js +162 -0
- package/templates/apply.html +928 -0
package/src/tui/index.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live progress view controller: owns the stderr repaint region during the
|
|
3
|
+
* default run, reduces pipeline progress events into render state, and tears
|
|
4
|
+
* itself down into today's plain line output.
|
|
5
|
+
*
|
|
6
|
+
* Contract (approved design, section "behavior"):
|
|
7
|
+
* - stderr only; stdout and --json are untouched
|
|
8
|
+
* - eligibility-gated: no TTY, NO_COLOR, CI, --quiet, --json, or a terminal
|
|
9
|
+
* narrower than 60 columns means the TUI never starts and nothing changes
|
|
10
|
+
* - while active, logger progress lines are buffered and replayed verbatim on
|
|
11
|
+
* teardown, so scrollback ends up exactly as it does without the TUI
|
|
12
|
+
* - all motion stops and the region is erased the moment the run ends
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { DEFAULT_MAX_EDITS } from "../proposal.js";
|
|
16
|
+
import { clearProgressSink, setProgressSink } from "../progress.js";
|
|
17
|
+
import { setLoggerSink } from "../logger.js";
|
|
18
|
+
import { colorDepth, detectBackground, tuiEligible } from "./term.js";
|
|
19
|
+
import { makeTheme } from "./theme.js";
|
|
20
|
+
import { SPINNER_INTERVAL_MS, renderFrame, spinnerFrame } from "./render.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Start the live view for one run. Returns null when the terminal is not
|
|
24
|
+
* eligible - callers treat that as "no TUI" and change nothing.
|
|
25
|
+
*/
|
|
26
|
+
export async function startTui(ctx, { stderr = process.stderr } = {}) {
|
|
27
|
+
if (!tuiEligible({ stderr, quiet: Boolean(ctx.flags.quiet), json: Boolean(ctx.flags.json) })) return null;
|
|
28
|
+
|
|
29
|
+
const depth = colorDepth({ stderr });
|
|
30
|
+
const preference = ctx.config.theme || "auto";
|
|
31
|
+
const background = preference === "auto" ? await detectBackground({ stderr }) : preference;
|
|
32
|
+
const theme = makeTheme({ depth, background });
|
|
33
|
+
|
|
34
|
+
const tui = new Tui({
|
|
35
|
+
theme,
|
|
36
|
+
stderr,
|
|
37
|
+
meta: {
|
|
38
|
+
version: ctx.version,
|
|
39
|
+
repoName: ctx.repo.name,
|
|
40
|
+
worktrees: ctx.repo.worktrees?.length || 1,
|
|
41
|
+
since: ctx.config.discovery.since,
|
|
42
|
+
maxEdits: ctx.config.maxEditsPerRun ?? DEFAULT_MAX_EDITS,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
tui.start();
|
|
46
|
+
return tui;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function initialState(meta) {
|
|
50
|
+
return {
|
|
51
|
+
meta,
|
|
52
|
+
memory: null,
|
|
53
|
+
discover: {
|
|
54
|
+
status: "pending",
|
|
55
|
+
startedAt: null,
|
|
56
|
+
endedAt: null,
|
|
57
|
+
order: [],
|
|
58
|
+
harnesses: {},
|
|
59
|
+
totalMatched: 0,
|
|
60
|
+
storesOk: 0,
|
|
61
|
+
storesTotal: 0,
|
|
62
|
+
files: 0,
|
|
63
|
+
newFiles: 0,
|
|
64
|
+
},
|
|
65
|
+
analyze: {
|
|
66
|
+
status: "pending",
|
|
67
|
+
startedAt: null,
|
|
68
|
+
endedAt: null,
|
|
69
|
+
total: 0,
|
|
70
|
+
pending: 0,
|
|
71
|
+
done: 0,
|
|
72
|
+
ok: 0,
|
|
73
|
+
skipped: 0,
|
|
74
|
+
failed: 0,
|
|
75
|
+
cached: 0,
|
|
76
|
+
jobs: 0,
|
|
77
|
+
agent: null,
|
|
78
|
+
model: null,
|
|
79
|
+
lanes: [],
|
|
80
|
+
evidence: { positive: 0, negative: 0, gaps: 0 },
|
|
81
|
+
},
|
|
82
|
+
fold: {
|
|
83
|
+
status: "pending",
|
|
84
|
+
startedAt: null,
|
|
85
|
+
endedAt: null,
|
|
86
|
+
instructions: 0,
|
|
87
|
+
clustersFound: 0,
|
|
88
|
+
clustersKept: 0,
|
|
89
|
+
minGapEvidence: 2,
|
|
90
|
+
},
|
|
91
|
+
synthesize: {
|
|
92
|
+
status: "pending",
|
|
93
|
+
startedAt: null,
|
|
94
|
+
endedAt: null,
|
|
95
|
+
agent: null,
|
|
96
|
+
model: null,
|
|
97
|
+
effort: null,
|
|
98
|
+
attempt: 0,
|
|
99
|
+
phase: "edit",
|
|
100
|
+
changes: null,
|
|
101
|
+
sessionName: null,
|
|
102
|
+
gapClusters: 0,
|
|
103
|
+
instructions: 0,
|
|
104
|
+
suppressed: 0,
|
|
105
|
+
violations: [],
|
|
106
|
+
edits: 0,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Reduce one progress event into the render state. Exported for tests. */
|
|
112
|
+
export function reduceEvent(state, event, data, now = Date.now()) {
|
|
113
|
+
const { discover: d, analyze: a, fold: f, synthesize: s } = state;
|
|
114
|
+
|
|
115
|
+
switch (event) {
|
|
116
|
+
case "memory":
|
|
117
|
+
state.memory = { path: data.path, tokens: data.tokens, budget: data.budget, units: data.units };
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "discover:start":
|
|
121
|
+
d.status = "active";
|
|
122
|
+
d.startedAt = now;
|
|
123
|
+
d.order = data.harnesses;
|
|
124
|
+
d.storesTotal = data.harnesses.length;
|
|
125
|
+
for (const harness of data.harnesses) {
|
|
126
|
+
d.harnesses[harness] = {
|
|
127
|
+
status: "pending",
|
|
128
|
+
scanned: 0,
|
|
129
|
+
total: 0,
|
|
130
|
+
matched: 0,
|
|
131
|
+
newCount: 0,
|
|
132
|
+
tiers: {},
|
|
133
|
+
error: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case "discover:harness:start":
|
|
138
|
+
d.harnesses[data.harness].status = "scanning";
|
|
139
|
+
break;
|
|
140
|
+
case "discover:harness:tick": {
|
|
141
|
+
const h = d.harnesses[data.harness];
|
|
142
|
+
h.scanned = data.scanned;
|
|
143
|
+
h.total = data.total;
|
|
144
|
+
h.matched = data.matched;
|
|
145
|
+
d.totalMatched = Object.values(d.harnesses).reduce((sum, row) => sum + row.matched, 0);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "discover:harness:done": {
|
|
149
|
+
const h = d.harnesses[data.harness];
|
|
150
|
+
if (data.error) {
|
|
151
|
+
h.status = "error";
|
|
152
|
+
h.error = data.error;
|
|
153
|
+
} else {
|
|
154
|
+
h.status = "done";
|
|
155
|
+
h.scanned = data.scanned;
|
|
156
|
+
h.matched = data.matched;
|
|
157
|
+
h.newCount = Math.max(data.scanned - (data.cached || 0), 0);
|
|
158
|
+
h.self = data.self || 0;
|
|
159
|
+
h.tiers = data.tiers || {};
|
|
160
|
+
d.storesOk += 1;
|
|
161
|
+
d.files += data.scanned;
|
|
162
|
+
d.newFiles += h.newCount;
|
|
163
|
+
}
|
|
164
|
+
d.totalMatched = Object.values(d.harnesses).reduce((sum, row) => sum + row.matched, 0);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "discover:done":
|
|
168
|
+
d.status = "done";
|
|
169
|
+
d.endedAt = now;
|
|
170
|
+
d.totalMatched = data.total;
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case "analyze:start":
|
|
174
|
+
a.status = "active";
|
|
175
|
+
a.startedAt = now;
|
|
176
|
+
a.total = data.total;
|
|
177
|
+
a.pending = data.pending;
|
|
178
|
+
a.cached = data.cached;
|
|
179
|
+
a.jobs = data.jobs;
|
|
180
|
+
a.agent = data.agent;
|
|
181
|
+
a.model = data.model;
|
|
182
|
+
a.lanes = new Array(Math.max(Math.min(data.jobs, data.pending), 0)).fill(null);
|
|
183
|
+
break;
|
|
184
|
+
case "analyze:lane": {
|
|
185
|
+
const existing = a.lanes[data.slot] || { startedAt: now };
|
|
186
|
+
a.lanes[data.slot] = { ...existing, ...data, startedAt: data.phase === "distill" ? now : existing.startedAt };
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "analyze:tick":
|
|
190
|
+
a.done = data.done;
|
|
191
|
+
a.ok = data.ok;
|
|
192
|
+
a.skipped = data.skipped;
|
|
193
|
+
a.failed = data.failed;
|
|
194
|
+
a.lanes[data.slot] = null;
|
|
195
|
+
break;
|
|
196
|
+
case "analyze:evidence":
|
|
197
|
+
a.evidence = { positive: data.positive, negative: data.negative, gaps: data.gaps };
|
|
198
|
+
break;
|
|
199
|
+
case "analyze:done":
|
|
200
|
+
a.status = "done";
|
|
201
|
+
a.endedAt = now;
|
|
202
|
+
a.ok = data.analyzed;
|
|
203
|
+
a.cached = data.cached;
|
|
204
|
+
a.skipped = data.skipped;
|
|
205
|
+
a.failed = data.failed;
|
|
206
|
+
a.lanes = [];
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
case "fold:done":
|
|
210
|
+
f.status = "done";
|
|
211
|
+
f.startedAt = now - (data.ms || 0);
|
|
212
|
+
f.endedAt = now;
|
|
213
|
+
f.instructions = data.instructions;
|
|
214
|
+
f.clustersFound = data.clustersFound;
|
|
215
|
+
f.clustersKept = data.clustersKept;
|
|
216
|
+
f.minGapEvidence = data.minGapEvidence;
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case "synth:start":
|
|
220
|
+
s.status = "active";
|
|
221
|
+
s.startedAt = now;
|
|
222
|
+
s.agent = data.agent;
|
|
223
|
+
s.model = data.model;
|
|
224
|
+
s.effort = data.effort;
|
|
225
|
+
s.attempt = data.attempt;
|
|
226
|
+
s.phase = data.phase || "edit";
|
|
227
|
+
s.changes = data.changes ?? null;
|
|
228
|
+
if (data.maxEdits) state.meta.maxEdits = data.maxEdits;
|
|
229
|
+
s.sessionName = data.sessionName;
|
|
230
|
+
s.gapClusters = data.gapClusters;
|
|
231
|
+
s.instructions = data.instructions;
|
|
232
|
+
s.suppressed = data.suppressed;
|
|
233
|
+
break;
|
|
234
|
+
case "synth:violations":
|
|
235
|
+
s.violations = data.violations || [];
|
|
236
|
+
break;
|
|
237
|
+
case "synth:done":
|
|
238
|
+
s.status = "done";
|
|
239
|
+
s.endedAt = now;
|
|
240
|
+
s.edits = data.edits;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const PAINT_THROTTLE_MS = 40;
|
|
247
|
+
|
|
248
|
+
class Tui {
|
|
249
|
+
constructor({ theme, stderr, meta }) {
|
|
250
|
+
this.theme = theme;
|
|
251
|
+
this.stderr = stderr;
|
|
252
|
+
this.state = initialState(meta);
|
|
253
|
+
this.buffered = [];
|
|
254
|
+
this.painted = 0;
|
|
255
|
+
this.lastPaint = 0;
|
|
256
|
+
this.stopped = false;
|
|
257
|
+
this.timer = null;
|
|
258
|
+
this.onEvent = this.onEvent.bind(this);
|
|
259
|
+
this.onResize = () => this.paint(true);
|
|
260
|
+
this.onSigint = () => {
|
|
261
|
+
this.stop();
|
|
262
|
+
process.exit(130);
|
|
263
|
+
};
|
|
264
|
+
this.onSigterm = () => {
|
|
265
|
+
this.stop();
|
|
266
|
+
process.exit(143);
|
|
267
|
+
};
|
|
268
|
+
this.onExit = () => {
|
|
269
|
+
if (!this.stopped) this.stderr.write("\x1b[?25h");
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
start() {
|
|
274
|
+
setProgressSink(this.onEvent);
|
|
275
|
+
// Progress lines keep being produced for scrollback; they are buffered here
|
|
276
|
+
// and replayed verbatim on teardown so the collapsed output is identical to
|
|
277
|
+
// a run without the TUI.
|
|
278
|
+
setLoggerSink((line) => this.buffered.push(line));
|
|
279
|
+
this.stderr.write("\x1b[?25l");
|
|
280
|
+
this.timer = setInterval(() => this.paint(), SPINNER_INTERVAL_MS);
|
|
281
|
+
this.stderr.on("resize", this.onResize);
|
|
282
|
+
process.on("SIGINT", this.onSigint);
|
|
283
|
+
process.on("SIGTERM", this.onSigterm);
|
|
284
|
+
process.once("exit", this.onExit);
|
|
285
|
+
this.paint(true);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
onEvent(event, data) {
|
|
289
|
+
reduceEvent(this.state, event, data);
|
|
290
|
+
this.paint();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
paint(force = false) {
|
|
294
|
+
if (this.stopped) return;
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
if (!force && now - this.lastPaint < PAINT_THROTTLE_MS) return;
|
|
297
|
+
this.lastPaint = now;
|
|
298
|
+
|
|
299
|
+
const lines = renderFrame(this.state, {
|
|
300
|
+
width: this.stderr.columns || 80,
|
|
301
|
+
theme: this.theme,
|
|
302
|
+
now,
|
|
303
|
+
spin: spinnerFrame(now),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
let out = "\x1b[?2026h";
|
|
307
|
+
if (this.painted > 0) out += `\x1b[${this.painted}A`;
|
|
308
|
+
out += "\r";
|
|
309
|
+
for (const line of lines) out += `\x1b[2K${line}\n`;
|
|
310
|
+
if (this.painted > lines.length) out += "\x1b[0J";
|
|
311
|
+
out += "\x1b[?2026l";
|
|
312
|
+
this.stderr.write(out);
|
|
313
|
+
this.painted = lines.length;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Idempotent teardown: erase the region, restore the cursor, replay buffered lines. */
|
|
317
|
+
stop() {
|
|
318
|
+
if (this.stopped) return;
|
|
319
|
+
this.stopped = true;
|
|
320
|
+
clearInterval(this.timer);
|
|
321
|
+
clearProgressSink();
|
|
322
|
+
setLoggerSink(null);
|
|
323
|
+
this.stderr.off("resize", this.onResize);
|
|
324
|
+
process.off("SIGINT", this.onSigint);
|
|
325
|
+
process.off("SIGTERM", this.onSigterm);
|
|
326
|
+
|
|
327
|
+
let out = "";
|
|
328
|
+
if (this.painted > 0) out += `\x1b[${this.painted}A\r\x1b[0J`;
|
|
329
|
+
out += "\x1b[?25h";
|
|
330
|
+
this.stderr.write(out);
|
|
331
|
+
this.painted = 0;
|
|
332
|
+
|
|
333
|
+
for (const line of this.buffered) this.stderr.write(`${line}\n`);
|
|
334
|
+
this.buffered = [];
|
|
335
|
+
}
|
|
336
|
+
}
|