bareloop 0.26.0 → 0.28.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.
@@ -54,7 +54,7 @@ import { seedListing, GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
54
54
  import { TOOL_MENU, WRITE_VERBS, STORE_VERBS } from './job.js';
55
55
  import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, strategyFor } from './tools.js';
56
56
  import { redactSecrets, SECRET_PATTERNS } from './validate.js';
57
- import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
57
+ import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop, callCasualty } from './text.js';
58
58
  import { PROVIDER_TIMEOUT_MS } from './clock.js';
59
59
 
60
60
  const require = createRequire(import.meta.url);
@@ -448,20 +448,25 @@ export async function runAuthorScout({
448
448
  * under the EXISTING `call-failed` cause. Without this seam a rejection would
449
449
  * escape uncaught to `run-author.mjs`'s top-level crash handler instead of the
450
450
  * honest, already-typed terminal this module was built to report through.
451
+ *
452
+ * The admitted-casualty test is `callCasualty` (`src/text.js`, beside
453
+ * `priceOf`) — the SAME predicate `askStructured` (`src/authorflow.js`, F179)
454
+ * uses for its own generate seam, widened past the idle-timeout class this
455
+ * comment originally named to also admit a malformed tool-call-arguments
456
+ * SyntaxError (F179's class): both are "a call that produced nothing", the
457
+ * definition `call-failed` already carries. Swallowing every throw would
458
+ * launder a budget HaltError, a programming bug, or any other named terminal
459
+ * into "the survey call failed" — the exact class of dishonesty this repo
460
+ * refuses (an unknown reported as a known) — so anything `callCasualty` does
461
+ * not admit still re-raises and is relayed after cleanup, as it was before
462
+ * this seam existed.
451
463
  * @param {Promise<any>} p @returns {Promise<any>}
452
464
  */
453
465
  const settled = async (p) => {
454
466
  try { return await p; } catch (e) {
455
- // ONLY the call's own deadline lands here. Swallowing every throw would
456
- // launder a budget HaltError, a programming bug, or any other named
457
- // terminal into "the survey call failed" — the exact class of dishonesty
458
- // this repo refuses (an unknown reported as a known). A timeout is the one
459
- // throw this seam exists for: it IS a call that produced nothing, which is
460
- // what `call-failed` already means. Everything else re-raises and is
461
- // relayed after cleanup, as it was before this seam existed.
462
- const err = /** @type {any} */ (e);
463
- if (err?.code !== 'ETIMEDOUT' && err?.name !== 'TimeoutError') throw e;
464
- return { error: String(err?.message ?? err) };
467
+ const reason = callCasualty(e);
468
+ if (reason === null) throw e;
469
+ return { error: reason };
465
470
  }
466
471
  };
467
472
 
package/src/cli.js CHANGED
@@ -33,7 +33,31 @@ import {
33
33
  exportBundle, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps,
34
34
  runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel, resolveJudge,
35
35
  } from './index.js';
36
- import { resolveProvider, buildRunnerProviders } from './providers.js';
36
+ import { resolveProvider, buildRunnerProviders, apiKeyProblem } from './providers.js';
37
+ // PANEL-BUILD.md P0 task 2/4 — `bareloop run-u` wraps the person-path run
38
+ // flow's own argv-parsing entry (`src/userrun.js`'s `main(argv, deps)`,
39
+ // itself the lift target of P0 task 1). This is the SAME function
40
+ // `scripts/run-u.mjs` calls directly — routing this command through it
41
+ // rather than re-parsing argv a second time here keeps exactly one code
42
+ // path for the flag grammar; `src/cli.js` only supplies the deps shape
43
+ // (`out`/`err`/`env`) this command's own callers already use.
44
+ import { main as runUMain } from './userrun.js';
45
+ // PANEL-BUILD.md P0 task 3/4 — `bareloop interview` and `bareloop author`
46
+ // wrap the close-authoring interview and authoring-pipeline flows' own
47
+ // argv-parsing entries (`src/interviewrun.js`/`src/authorrun.js`, lifted out
48
+ // of `scripts/run-interview.mjs`/`scripts/run-author.mjs` the same way task
49
+ // 1/2 lifted `scripts/run-u.mjs`). `scripts/run-interview.mjs`/`scripts/
50
+ // run-author.mjs` call these SAME functions directly, so there is exactly
51
+ // one place each flag grammar is parsed.
52
+ import { main as interviewMain } from './interviewrun.js';
53
+ import { main as authorMain } from './authorrun.js';
54
+ // PANEL-BUILD.md P0 — the spine/gate-audit read side (`bareloop replay`,
55
+ // and `doHistory`'s reader below), lifted out of `scripts/run-replay.mjs`
56
+ // into `src/replayio.js` the same way tasks 1-3 lifted their own scripts.
57
+ import {
58
+ parseJsonl, looksLikeSpine, replayOne, listSpines, readHistoryLog,
59
+ } from './replayio.js';
60
+ import { formatReplay, formatAllLines } from './replay.js';
37
61
 
38
62
  // The tier->model tables live in `src/providers.js` now (PRD item 28's
39
63
  // factory) — one seam instead of a copy hardcoded in each runner. A
@@ -275,6 +299,15 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
275
299
  out(`bundleHash: ${bundle.manifest.bundleHash}`);
276
300
  return 0;
277
301
  }
302
+ // F181 — a key that reads as "set" above can still carry a line break/
303
+ // control char/stray whitespace and crash Node's own header-encode
304
+ // inside the paid span. Refused here, at the same door, before any
305
+ // provider is constructed; never echoes the value.
306
+ const keyProblem = apiKeyProblem(apiKey);
307
+ if (keyProblem) {
308
+ err(`ANTHROPIC_API_KEY ${keyProblem} — refusing rather than crashing mid-call (never trimmed or repaired; fix the value at its source)`);
309
+ return 1;
310
+ }
278
311
  try {
279
312
  ({ provider, providerFor, judgeProvider, judgeModel } = buildProviders(apiKey, bundle.spec));
280
313
  } catch (e) {
@@ -388,8 +421,12 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
388
421
  if (existsSync(auditSrc)) renameSync(auditSrc, join(runsDir, 'gate-audit.jsonl'));
389
422
 
390
423
  // the job-end record off THIS run's own spine — never fabricate a 0 for an
391
- // unknown spend (F6/F12's class).
392
- const events = readFileSync(spineFile, 'utf8').trimEnd().split('\n').filter(Boolean).map((l) => JSON.parse(l));
424
+ // unknown spend (F6/F12's class). PANEL-BUILD.md P0: read via the same
425
+ // named library reader `doHistory`/`doReplay` already use (`parseJsonl`,
426
+ // `src/replayio.js`) instead of hand-rolling the parse again here — it
427
+ // tolerates a malformed/truncated line (a process killed mid-append)
428
+ // instead of throwing uncaught and crashing this run's exit tail.
429
+ const { records: events } = parseJsonl(spineFile);
393
430
  const je = events.findLast((/** @type {any} */ e) => e.type === 'job-end');
394
431
  const spentUsd = je?.spentUsd ?? null;
395
432
  const spendComplete = je?.spendComplete ?? null;
@@ -441,10 +478,17 @@ function doHistory(args, { out, err, cwd }) {
441
478
  const dir = resolve(cwd, bundleDirArg);
442
479
  const historyFile = join(dir, 'history.jsonl');
443
480
  if (existsSync(historyFile)) {
481
+ // PANEL-BUILD.md P0 — the one reader for `history.jsonl` (`src/replayio.js`'s
482
+ // `readHistoryLog`), replacing this command's own hand-rolled
483
+ // `readFileSync`/split. Each row is re-printed with `JSON.stringify`
484
+ // (byte-identical to the original trimmed line for every well-formed
485
+ // row, since `appendHistory` itself writes `JSON.stringify(row)` —
486
+ // src/bundle.js:712); a malformed line is counted, never silently
487
+ // dropped or reprinted corrupt.
488
+ const { rows, skipped } = readHistoryLog(historyFile);
444
489
  out('history:');
445
- for (const line of readFileSync(historyFile, 'utf8').split('\n')) {
446
- if (line.trim()) out(` ${line.trim()}`);
447
- }
490
+ for (const row of rows) out(` ${JSON.stringify(row)}`);
491
+ if (skipped > 0) out(` (${skipped} malformed line${skipped === 1 ? '' : 's'} skipped)`);
448
492
  } else out('history: (no runs yet)');
449
493
  const reg = loadRegistry(join(dir, 'bridges'));
450
494
  if (reg.bridges.length > 0) {
@@ -454,9 +498,56 @@ function doHistory(args, { out, err, cwd }) {
454
498
  return 0;
455
499
  }
456
500
 
501
+ /**
502
+ * `bareloop replay <spine.jsonl>` — one run's whole story, reconstructed
503
+ * from its spine + gate-audit sidecar, printed as one page
504
+ * ({@link formatReplay}). `bareloop replay --all <dir>` — every spine found
505
+ * directly in `dir`, one aligned table row each ({@link formatAllLines}).
506
+ * Read-only, $0, mints no verdict, writes nothing (PANEL-BUILD.md P0: this
507
+ * is the CLI the gap table marked "No" for "Replay an archived run at $0").
508
+ * Logic lifted verbatim from `scripts/run-replay.mjs` into
509
+ * `src/replayio.js`; this command is that script's one caller now, the same
510
+ * shape `bareloop history`/`bareloop run` already are over their own
511
+ * library functions.
512
+ * @param {string[]} args @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string }} ctx
513
+ */
514
+ function doReplay(args, { out, err, cwd }) {
515
+ if (args[0] === '--all') {
516
+ const dirArg = args[1];
517
+ if (!dirArg) { err('usage: bareloop replay --all <dir>'); return 1; }
518
+ const dir = resolve(cwd, dirArg);
519
+ if (!existsSync(dir)) { err(`no such directory: ${dir}`); return 1; }
520
+ const entries = listSpines(dir);
521
+ if (entries.length === 0) { out(`no .jsonl files found in ${dir}`); return 0; }
522
+ out(formatAllLines(entries));
523
+ return 0;
524
+ }
525
+ const spinePathArg = args[0];
526
+ if (!spinePathArg) {
527
+ err('usage: bareloop replay <spine.jsonl>');
528
+ err(' bareloop replay --all <dir>');
529
+ return 1;
530
+ }
531
+ const spinePath = resolve(cwd, spinePathArg);
532
+ if (!existsSync(spinePath)) { err(`no such file: ${spinePath}`); return 1; }
533
+ const spine = parseJsonl(spinePath);
534
+ if (!looksLikeSpine(spine.records)) {
535
+ err(`${spinePath} does not look like a spine (no job-start or run-start record found)`);
536
+ return 1;
537
+ }
538
+ const summary = replayOne(spinePath, { preParsedSpine: spine });
539
+ out(formatReplay(summary));
540
+ return 0;
541
+ }
542
+
457
543
  /**
458
544
  * The bare `bareloop` menu: `1 export 2 run 3 history q quit`, then the
459
- * SAME code path as the sub-command, asking its args line by line.
545
+ * SAME code path as the sub-command, asking its args line by line. `run-u`,
546
+ * `interview`, `author` and `replay` are NOT wizarded here — a line-at-a-time
547
+ * prompt for `run-u`'s 13 flags (or the others' own argv shapes) would be
548
+ * inventing new interactive UX, not wiring, and that judgement stands. This
549
+ * menu is fixed instead: it now says those four commands exist and how to
550
+ * reach them, rather than implying only three commands do at all.
460
551
  * @param {any} deps @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string, env: any, now: () => number }} ctx
461
552
  */
462
553
  async function runMenu(deps, ctx) {
@@ -465,6 +556,7 @@ async function runMenu(deps, ctx) {
465
556
  const rl = createInterface({ input: stdin, output: stdout });
466
557
  try {
467
558
  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)');
468
560
  const choice = (await rl.question('> ')).trim().toLowerCase();
469
561
  if (choice === '' || choice === 'q' || choice === 'quit') return 0;
470
562
  if (choice === '1') {
@@ -519,6 +611,36 @@ export async function main(argv, deps = {}) {
519
611
  if (cmd === 'export') return doExport(rest, ctx);
520
612
  if (cmd === 'run') return doRun(rest, ctx);
521
613
  if (cmd === 'history') return doHistory(rest, ctx);
522
- err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history`);
614
+ // `bareloop run-u` — the person-path run flow (PANEL-BUILD.md P0 task
615
+ // 2/4). `rest` is handed straight to `src/userrun.js`'s own `main(argv,
616
+ // deps)`, unparsed: that function is this flag grammar's one owner
617
+ // (`--job`/`--spec`/`--resume`/`--door`/`--decide`/`--text`/
618
+ // `--review-door`/`--approve`/`--registry`/`--workflow`/`--model`/
619
+ // `--read-shim`/`--scout`). `deps` is spread first so an injected test
620
+ // seam (`deps.provider`/`providerFor`/`judgeProvider`) still reaches it,
621
+ // then `env`/`out`/`err` are overridden to the SAME resolved values every
622
+ // other command here prints through, so `run-u`'s output lands on the
623
+ // `stdout`/`stderr` a caller of `main` actually passed.
624
+ if (cmd === 'run-u') return runUMain(rest, { ...deps, env, out, err });
625
+ // `bareloop interview` — the close-authoring interview (PANEL-BUILD.md P0
626
+ // task 3/4). This flow reads a TTY (or a piped stdin) directly rather than
627
+ // through the `out`/`err` line-functions every other command here uses, so
628
+ // it needs the raw streams, not the wrapped ones — `stdout`/`stderr` (both
629
+ // already resolved above) are handed through instead.
630
+ // `invokedAs: 'bareloop interview'` — the usage message names the command a
631
+ // person actually typed, never a script path they never invoked and may
632
+ // not have on disk (a branch-review nit; `scripts/run-interview.mjs` still
633
+ // supplies none, so it keeps naming itself when run directly).
634
+ if (cmd === 'interview') return interviewMain(rest, { ...deps, env, stdin: deps.stdin ?? process.stdin, stdout, stderr, invokedAs: 'bareloop interview' });
635
+ // `bareloop author` — the authoring pipeline (scout, declaration, D9's
636
+ // gates), same task. Same raw-stream reasoning: its one interactive seam
637
+ // (the confirm turn) reads stdin directly. Same `invokedAs` reasoning too.
638
+ if (cmd === 'author') return authorMain(rest, { ...deps, env, stdin: deps.stdin ?? process.stdin, stdout, stderr, invokedAs: 'bareloop author' });
639
+ // `bareloop replay` — the spine/gate-audit read side (PANEL-BUILD.md P0,
640
+ // last of the rung's four tasks). Synchronous, file-based, no interactive
641
+ // seam — same shape as `doHistory`/`doExport`, not the argv-owning
642
+ // `main(argv, deps)` modules the flows above delegate to.
643
+ if (cmd === 'replay') return doReplay(rest, ctx);
644
+ err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history, run-u, interview, author, replay`);
523
645
  return 1;
524
646
  }
package/src/index.js CHANGED
@@ -237,7 +237,19 @@ export { runBehaviour, formatBehaviour } from './behaviour.js';
237
237
  // (`replayRun`) or one printable page (`formatReplay`). Reads only, mints no
238
238
  // verdict, writes nothing to the spine — reuses `runBehaviour` rather than
239
239
  // re-implementing the tool-call count.
240
- export { replayRun, formatReplay } from './replay.js';
240
+ export { replayRun, formatReplay, summarizeForAllLine, formatAllLines } from './replay.js';
241
+ // PANEL-BUILD.md P0 — the IO layer over `replayRun`: read one run's spine
242
+ // (+ its gate-audit sibling) off disk, or list every spine directly in a
243
+ // directory (content-detected, never by filename pattern — see the module
244
+ // header) as `--all`-style rows. `readHistoryLog` is the one reader for a
245
+ // bundle's `history.jsonl`, replacing `src/cli.js`'s own former hand-rolled
246
+ // read. `src/replayio.js` also exports lower-level plumbing this package
247
+ // root deliberately does NOT re-export (`parseJsonl`, `isSidecarByName`,
248
+ // `looksLikeSpine`, `resolveSiblings`): a raw JSONL parser, a filename
249
+ // predicate, and a sibling-path resolver are internal steps these three
250
+ // functions already compose for a caller — not their own adopter-facing
251
+ // promise, and an export here is a promise kept forever.
252
+ export { replayOne, listSpines, readHistoryLog } from './replayio.js';
241
253
  // M4b — the interview, the refusal, the composition, and D9's three gates.
242
254
  // `prepareSigning` returns the resolved spec's HASH and the seed evidence; it
243
255
  // never signs — the approvals array and the human's word are unchanged.
@@ -291,5 +303,5 @@ export { main as cliMain } from './cli.js';
291
303
  // module, not a re-derivation of it.
292
304
  export {
293
305
  prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest,
294
- datedDestination, pickDelivery,
306
+ datedDestination, pickDelivery, missingDependencies,
295
307
  } from './source.js';