bareloop 0.28.0 → 0.29.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/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ All notable changes to bareloop are documented here. Format:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
+ ## [0.29.0] — 2026-09-26
9
+
10
+ ### Added
11
+
12
+ - **`bareloop` panel (N6/P0/P1)** — a read-only HTTP server (`bareloop panel`, bound to
13
+ `127.0.0.1:4700`) serving a single-page app built from one run list
14
+ (`~/.config/bareloop/runs.jsonl`, populated by `bareloop run`/`bareloop runs backfill`).
15
+ The page reads the library's own `replayRun`/spine/gate-audit functions directly, in-process
16
+ — the same read side `bareloop replay` uses, never a duplicated parser.
17
+ - **Run tab: step-map + per-part cards.** A plan-shape run renders one two-line card per PART
18
+ (scout, plan, each step, each replan, the post-step fix loop, judge when recorded), each
19
+ clickable through to its own slice of the Audit tab; an old-shape (iterations, no
20
+ step-start) run collapses to one "run" card.
21
+ - **Audit tab: Grouped (part → attempt → rounds table) and Flat views**, a search box
22
+ (matches job/runid/model), and All/Writes/Blocked filter chips that auto-open every
23
+ matching group.
24
+ - **Job tab** resolves the real spec (bundle `spec.json`, `jobs/<job>.json`, or the run's own
25
+ job-start record) and shows Success/Guardrails/Tools/Judge-examples fields plus the full
26
+ granted-tools "offered" list.
27
+ - **Run Summary** gained `model` (with provider) and `judge` lines, and a died glyph `[?]`
28
+ distinct from a real failed verdict `[✗]`.
29
+
30
+ ### Changed
31
+
32
+ - **Workflows and History merged into one `Runs` tab**, toggled between a grouped-by-job
33
+ "Workflows" view (default) and a flat "History" view — both built from the one `/api/runs`
34
+ list; `/api/workflows` and the client's `listWorkflows` were deleted.
35
+ - Audit-tab paths shorten to the run's own resolved tree root (`pathShort`) when one can be
36
+ found; an older run with no resolvable root keeps its full absolute path.
37
+
38
+ ### Fixed
39
+
40
+ - **`bareloop replay`'s tool-call count was contaminated by a shared gate-audit sidecar** — a
41
+ sidecar file can carry rows from several runs that happen to reuse the same filename;
42
+ `auditWindow` now scopes every reader (CLI and panel alike) to the run's own
43
+ `job-start`..`job-end` window, so the two can never disagree.
44
+ - **Panel tests bound a fixed port, flaking under a taken port** — every test now asks the OS
45
+ for an ephemeral port (`port: 0`) instead of guessing a free one.
46
+ - Audit tab's "Raw log" leaked an EARLIER unrelated run's rows when a gate-audit sidecar is
47
+ shared across runs; it now shows only this run's own ts-windowed lines, byte-for-byte.
48
+ - A `~2`-style backfill-collision runid (two archived spines sharing a basename in different
49
+ directories) echoed the wrong, unsuffixed runid in the run header and its sibling lookups.
50
+ - `toolCalls`/`behaviour` report `null` (unknown), never a fabricated `0`, when a run has no
51
+ gate-audit sidecar at all.
52
+ - Backfill scans recursively (bounded depth) instead of the top directory plus its immediate
53
+ subdirectories only, and dedups by resolved spine path rather than a filename-derived runid
54
+ (two same-basename spines in different directories are different runs, not duplicates).
55
+ - **Workflows job (parent) row is its latest run**: clicking it opens that run (previously a
56
+ dead click after the Runs-tab merge); the expanded child list shows only OLDER runs, never
57
+ repeating the latest as its own first child; the expand caret appears only when a matching
58
+ older run exists under the active filters/search (not just `runCount > 1`); and the row's
59
+ "selected" state and click target both follow whichever run is actually active
60
+ (`representedRun`) — so a search hit or a History pick on an older run correctly expands
61
+ and highlights that run's job, instead of always marking/opening the job's latest.
62
+ - Various Workflows/History/Audit rendering fixes: rows stay two lines and never wrap; the
63
+ meta-line separator carries a leading space; a died run's "why" line uses one plain
64
+ timestamp format; a run that died before its first step reads "0 of 0 done" (never "0 of
65
+ 1"); the Audit tab distinguishes "no sidecar ever written" from "sidecar exists but empty";
66
+ History's filter chips no longer needed two clicks to register; the desktop list scrolls in
67
+ its own pane and mobile gets a "back to list" affordance; check-type labelling correctly
68
+ falls back to "deterministic" for runs that predate `verdictType` (2026-08-18); no-verdict
69
+ parts (scout/plan/replan/judge) render no result glyph rather than a guessed one; the Run
70
+ tab's step-map numbering and card order matched (F197/F198); Summary's "offered" line shows
71
+ the FULL granted-tools list, never truncated to the top few.
72
+
8
73
  ## [0.28.0] — 2026-09-24
9
74
 
10
75
  ### Added
@@ -3186,6 +3186,63 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3186
3186
  this command reports, it never grades), `1` on a missing file/directory or a non-spine
3187
3187
  file, `1` with a usage line on no args.
3188
3188
 
3189
+ - **`bareloop runs`** / **`bareloop runs backfill <dir>`** (PANEL-BUILD.md P1, 2026-09-24
3190
+ rulings — "one home for runs", hamr's option B) → the one run list, at `~/.config/bareloop/
3191
+ runs.jsonl` (the same directory the keys file, PRD §7d, lives in — outside any repo tree,
3192
+ never moved into one). This is a LIST, not a relocation: patient copies stay exactly where
3193
+ they already are (`bareloop-patients/…` for `run-u`, `<bundleDir>/runs/<runid>/` for
3194
+ `bareloop run`); jobs stay in `jobs/` (moving them into the home too is deferred to P3,
3195
+ hamr picked option "A"). `bareloop runs` with no subcommand prints every listed row, one
3196
+ line each — `job (runid) · date · spine path`, with ` — file missing` appended when the
3197
+ row's `spine` no longer exists on disk. `bareloop runs backfill <dir>` scans `dir` and its
3198
+ immediate subdirectories for archived spines already on disk (both the free-standing
3199
+ layout, e.g. `<dir>/<x>/u-<id>.jsonl`, and the bundle layout, `<dir>/<x>/runs/<runid>/
3200
+ spine.jsonl`), reusing `src/replayio.js`'s `parseJsonl`/`isSidecarByName`/`looksLikeSpine`
3201
+ (never a second parser), and adds one row per spine whose runid is not already listed —
3202
+ idempotent, running it twice adds nothing the second time. Prints `added N, already listed
3203
+ M, skipped K (not a spine or unreadable)` — never silent about what it found. Read-only,
3204
+ $0: no interview, no author, no run trigger, no key ever read. A row is `{ at, runid, job,
3205
+ spine, patient, via }` — `spine`/`patient` are always absolute paths (`patient: null` when
3206
+ not known, e.g. every `via:"backfill"` row: no spine record carries a run's workdir);
3207
+ `via` is `'run-u'`, `'bundle'`, or `'backfill'`. `bareloop run-u` and `bareloop run` (the
3208
+ bundle path) each append their own row at run START, before the first paid call — a
3209
+ list-append failure is caught at both call sites and printed loudly to stderr
3210
+ (`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (…)`); the run itself
3211
+ always continues (a panel list must never block real, already-signed work). Interview/
3212
+ author sessions are NOT added (deferred to P3 — nothing to list at that stage yet). These
3213
+ functions (`appendRun`/`readRunList`/`backfillRuns`/`formatRunRow`, `src/runlist.js`) are
3214
+ NOT exported from the package root — the panel's HTTP server (P1's next task) will call
3215
+ them the same in-process way `src/cli.js` does, per the layering law
3216
+ (`docs/product/PANEL-BUILD.md` §2), not through `bareloop`'s public API.
3217
+
3218
+ - **`bareloop panel [--port N]`** (PANEL-BUILD.md P1) → a READ-ONLY `node:http` server
3219
+ (no new dependency), bound to `127.0.0.1` ONLY, default port `4700`. Serves the panel's
3220
+ look (Tokyo Night box-drawing TUI style) over the run list's real data: `GET /` the page;
3221
+ `GET /api/runs` (newest first, each row enriched with a glyph/check-type/spend/wall
3222
+ summary, `fileMissing:true` when the listed spine is gone — the client groups this SAME
3223
+ list by job for the left pane's "Workflows" view and renders it flat for "History", both
3224
+ behind one `Runs` tab toggle; there is no separate `/api/workflows` endpoint, deleted in
3225
+ the P1 Runs-tab merge); `GET /api/runs/:runid` (the full replay — parts/steps, counters,
3226
+ summary, model/provider/judge); `GET /api/runs/:runid/audit` (the gate-audit sidecar rows,
3227
+ ts-windowed to this run alone even when the sidecar file is shared with other runs, plus
3228
+ the matching raw-log text windowed the same way — when one resolves); `GET
3229
+ /api/runs/:runid/rounds?part=&attempt=&offset=&limit=` (one attempt's rounds, lazily
3230
+ paged, for the Audit tab's Grouped view); `GET /api/runs/:runid/job` (the signed spec's
3231
+ own fields, ONLY when a bundle-layout run's `spec.json` is reachable — a run-u run has
3232
+ none on disk, and this reads `resolved:false` with every spec-only field honestly
3233
+ `'unknown'`, never guessed from the spine's own narrower `job-start` record). Every
3234
+ endpoint is GET/HEAD only (anything else — including every write verb — is `405`); a URL
3235
+ never joins a path segment into a filesystem read — a runid is looked up in the run list
3236
+ first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
3237
+ suffix too, and every endpoint echoes back the LISTED runid, never a filename-derived one
3238
+ that can silently drop that suffix), and only the path THAT ROW stores is ever read. A
3239
+ taken port fails loudly (names the port, exit `1`) — this command never silently tries a
3240
+ different one. No interview, no author, no run trigger, no key/`.env` ever read on this
3241
+ path. One more caller of `src/replayio.js`/`src/runlist.js`, in-process, per the layering
3242
+ law (`docs/product/PANEL-BUILD.md` §2) — the panel's HTTP handler holds no flow logic of
3243
+ its own. Chat/Settings/authoring (P3/P4) are not built yet; the page says so rather than
3244
+ hiding the gap.
3245
+
3189
3246
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3190
3247
  (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
3191
3248
  U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/authorrun.js CHANGED
@@ -1277,7 +1277,15 @@ export async function main(argv, deps = {}) {
1277
1277
  err(` AND THE SPINE COULD NOT BE WRITTEN: ${/** @type {any} */ (spineErr)?.message ?? spineErr}`);
1278
1278
  err(' the error above is the only record of this run — copy it before it scrolls');
1279
1279
  }
1280
- err(` spine ${spineFile}`);
1280
+ // no `err(\` spine ${spineFile}\`)` here — this catch falls through to the
1281
+ // shared tail (`out(\`\nspine ${spineFile}\`)\`, below), which runs on
1282
+ // EVERY exit path including this one (nothing in finally/the tail skips it
1283
+ // when the catch already set exitCode). A second, catch-local print here
1284
+ // used to duplicate that line on every crash; ONE owner (the tail) now
1285
+ // prints it, same as the refusal/failed-gate/signed-readout paths always
1286
+ // did. (The `err(' spine …')` in the SIGINT/SIGTERM/SIGHUP handler above
1287
+ // is a different path — a real signal re-raises and never reaches the
1288
+ // tail, so that one stays as the sole print for a killed run.)
1281
1289
  // 4, distinct from 1 (a refusal or a failed gate), 2 (operator/config) and 3 (a
1282
1290
  // leak). A crash is none of those: it is the run failing to reach a verdict at
1283
1291
  // all, and sharing an exit code with a refusal would file a bug as a result.
package/src/behaviour.js CHANGED
@@ -126,11 +126,14 @@ export function runBehaviour(events, { runId } = {}) {
126
126
  * Render a {@link runBehaviour} summary as the agreed printable block
127
127
  * (2–4 short lines). A zero-call run formats plainly ("0 tool calls") rather
128
128
  * than a NaN-laced percentage — an empty run is a complete, reportable
129
- * answer, not a division error.
130
- * @param {ReturnType<typeof runBehaviour>} summary
129
+ * answer, not a division error. `summary === null` means the caller never
130
+ * had a gate-audit sidecar to read at all (see `replayRun`'s `auditAvailable`
131
+ * option) — that reads as "unknown", never coerced to a 0-call summary.
132
+ * @param {ReturnType<typeof runBehaviour>|null} summary
131
133
  * @returns {string}
132
134
  */
133
135
  export function formatBehaviour(summary) {
136
+ if (summary === null) return 'unknown (no gate-audit sidecar found for this run)';
134
137
  const { totalCalls, byTool, denied, repeats, repeatPct } = summary; // uniquePaths: data-only, not in the agreed printed shape
135
138
  const lines = [];
136
139
 
package/src/cli.js CHANGED
@@ -58,6 +58,17 @@ import {
58
58
  parseJsonl, looksLikeSpine, replayOne, listSpines, readHistoryLog,
59
59
  } from './replayio.js';
60
60
  import { formatReplay, formatAllLines } from './replay.js';
61
+ // PANEL-BUILD.md P1 — the read-only panel HTTP server (`bareloop panel`).
62
+ // One more caller of the same `src/` library functions this file already
63
+ // calls (the layering law, PANEL-BUILD.md §2) — `src/panel/server.js` never
64
+ // re-implements the spine/run-list read side, it calls `src/replayio.js`/
65
+ // `src/runlist.js` directly, in-process.
66
+ import { panelMain } from './panel/server.js';
67
+ // PANEL-BUILD.md P1 (2026-09-24 rulings) — the one run list
68
+ // (`~/.config/bareloop/runs.jsonl`) and its backfill scan. `bareloop runs`
69
+ // (list) and `bareloop runs backfill <dir>` (reconstruct rows from archived
70
+ // spines already on disk) are this rung's only new commands.
71
+ import { appendRun, readRunList, backfillRuns, formatRunRow } from './runlist.js';
61
72
 
62
73
  // The tier->model tables live in `src/providers.js` now (PRD item 28's
63
74
  // factory) — one seam instead of a copy hardcoded in each runner. A
@@ -393,6 +404,23 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
393
404
  const closeDir = join(runsDir, 'close');
394
405
  mkdirSync(closeDir, { recursive: true });
395
406
 
407
+ // PANEL-BUILD.md P1 — one row in the run list, BEFORE the first paid call
408
+ // (runJob, right below). A list-append failure must never block a real,
409
+ // already-signed run: caught and named loudly, never rethrown.
410
+ // `deps.runlistHome` — the SAME test seam shape `deps.provider`/`deps.now`
411
+ // already are on this ctx (never a new env var, see src/runlist.js's own
412
+ // header comment): a caller that injects it (every test in
413
+ // tests/cli.test.js that reaches this line) writes its row under a temp
414
+ // home instead of the real `~/.config/bareloop`; production never sets it,
415
+ // so `appendRun` falls through to `runlistHome()`'s own `os.homedir()`.
416
+ try {
417
+ appendRun({
418
+ at: new Date(now()).toISOString(), runid, job: runSpec.job, spine: spineFile, patient: worktree, via: 'bundle',
419
+ }, { home: deps.runlistHome });
420
+ } catch (e) {
421
+ err(`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (${/** @type {Error} */ (e).message}) — the run continues; the panel's list will be missing this row.`);
422
+ }
423
+
396
424
  let outcome;
397
425
  try {
398
426
  outcome = await runJob(runSpec, {
@@ -540,6 +568,38 @@ function doReplay(args, { out, err, cwd }) {
540
568
  return 0;
541
569
  }
542
570
 
571
+ /**
572
+ * `bareloop runs` — print the one run list, one line per row
573
+ * ({@link formatRunRow}). `bareloop runs backfill <dir>` — scan `dir` for
574
+ * archived spines and add a row per one not already listed
575
+ * ({@link backfillRuns}), printing the count added/already-listed/skipped
576
+ * (never silent). Read-only, $0: no interview, no author, no run trigger, no
577
+ * key ever read.
578
+ * @param {string[]} args @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string }} ctx
579
+ */
580
+ function doRuns(args, { out, err, cwd }) {
581
+ if (args[0] === 'backfill') {
582
+ const dirArg = args[1];
583
+ if (!dirArg) { err('usage: bareloop runs backfill <dir>'); return 1; }
584
+ const dir = resolve(cwd, dirArg);
585
+ if (!existsSync(dir)) { err(`no such directory: ${dir}`); return 1; }
586
+ let result;
587
+ try {
588
+ result = backfillRuns(dir);
589
+ } catch (e) {
590
+ err(`backfill failed: ${/** @type {Error} */ (e).message}`);
591
+ return 1;
592
+ }
593
+ out(`backfill ${dir}: added ${result.added}, already listed ${result.alreadyListed}, skipped ${result.skipped} (not a spine or unreadable)`);
594
+ return 0;
595
+ }
596
+ const { rows, skipped } = readRunList();
597
+ if (rows.length === 0) { out('runs: (none listed yet — see `bareloop runs backfill <dir>`)'); }
598
+ else for (const row of rows) out(formatRunRow(row));
599
+ if (skipped > 0) out(`(${skipped} malformed line${skipped === 1 ? '' : 's'} skipped)`);
600
+ return 0;
601
+ }
602
+
543
603
  /**
544
604
  * The bare `bareloop` menu: `1 export 2 run 3 history q quit`, then the
545
605
  * SAME code path as the sub-command, asking its args line by line. `run-u`,
@@ -556,7 +616,7 @@ async function runMenu(deps, ctx) {
556
616
  const rl = createInterface({ input: stdin, output: stdout });
557
617
  try {
558
618
  ctx.out('1 export 2 run 3 history q quit');
559
- ctx.out('(also on the command line, not wizarded here: run-u, interview, author, replay — run `bareloop <name>` with its own flags)');
619
+ ctx.out('(also on the command line, not wizarded here: run-u, interview, author, replay, runs, panel — run `bareloop <name>` with its own flags)');
560
620
  const choice = (await rl.question('> ')).trim().toLowerCase();
561
621
  if (choice === '' || choice === 'q' || choice === 'quit') return 0;
562
622
  if (choice === '1') {
@@ -593,7 +653,7 @@ async function runMenu(deps, ctx) {
593
653
  * code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
594
654
  * queued stdout on its own (F-something: `process.exit()` can discard it).
595
655
  * @param {string[]} argv
596
- * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any }} deps
656
+ * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string }} deps
597
657
  * @returns {Promise<number>}
598
658
  */
599
659
  export async function main(argv, deps = {}) {
@@ -641,6 +701,15 @@ export async function main(argv, deps = {}) {
641
701
  // seam — same shape as `doHistory`/`doExport`, not the argv-owning
642
702
  // `main(argv, deps)` modules the flows above delegate to.
643
703
  if (cmd === 'replay') return doReplay(rest, ctx);
644
- err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history, run-u, interview, author, replay`);
704
+ // `bareloop runs` / `bareloop runs backfill <dir>` — PANEL-BUILD.md P1's
705
+ // one run list. Read-only ($0): no interview, no author, no run trigger,
706
+ // no key ever read.
707
+ if (cmd === 'runs') return doRuns(rest, ctx);
708
+ // `bareloop panel [--port N]` — PANEL-BUILD.md P1's read-only HTTP panel.
709
+ // Never runs a job, spends money, or reads a key: it serves the run list
710
+ // and spine/gate-audit reads over `127.0.0.1` only. `deps.runlistHome`
711
+ // rides through the same injectable seam `run`/`run-u` already use.
712
+ if (cmd === 'panel') return panelMain(rest, { out, err, runlistHome: deps.runlistHome });
713
+ err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history, run-u, interview, author, replay, runs, panel`);
645
714
  return 1;
646
715
  }