bareloop 0.19.1 → 0.21.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,96 @@ 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.21.0] — 2026-09-06
9
+
10
+ ### Added
11
+
12
+ - **Export/run a bundle** (`docs/product/EXPORT-BUILD.md`, frozen 2026-09-05). New
13
+ `src/bundle.js`: `exportBundle` mints a plain directory bundle (signed spec with
14
+ `close[].cmd` rewritten under `$BARELOOP_BUNDLE`, the close scripts, and the job's
15
+ registry history) from a job that already carries a real green at its current spec
16
+ hash — refuses (typed reds, nothing written) on an unrelocatable close cmd, a script
17
+ collision, an import this module cannot verify or that bareloop's own `src/index.js`
18
+ does not export, or no bridge at the spec's hash. `bundleHash(dir)` / `readBundle(dir)`
19
+ recompute the signature from disk and red `bundle-tampered` on any mismatch — the load-
20
+ bearing check that stands between a swapped close script and a fake green (N4).
21
+ `resolveBundleSpec`, `checkEnvelope` (tighten-only budget/wall), `bless`/`verifyBlessing`
22
+ (first-run approval, no-resign on later runs), and `appendHistory` round out the module.
23
+ - **`bareloop` CLI and `bin/`.** New `bin/bareloop.mjs` (the package's first `bin` entry)
24
+ and `src/cli.js`'s `main(argv, deps)` — exported from the package root as `cliMain` —
25
+ implement `bareloop export|run|history` plus a bare numbered menu. `run` orders its
26
+ steps as: tamper check, tighten-only envelope check, provider key (or the injected
27
+ test-seam provider), blessing/approval, a fresh `git worktree add --detach` per run,
28
+ then `runJob` itself; the tail prints the outcome, spend, and the `git merge` command —
29
+ merge stays human, the CLI never merges. `bin/` added to `package.json`'s `files`.
30
+ - **`checkBundleDeps(bundleDir)`** (`src/bundle.js`, F128): resolves `bareloop` from the
31
+ bundle's own `close/` directory (`createRequire(...).resolve('bareloop')`, walking the
32
+ package's own `exports` map) and reds typed `bundle-deps-missing` — with the exact cure
33
+ line `cd <bundleDir> && npm install` — when it cannot. `bareloop run` calls it as literal
34
+ step 1b, right after the tamper check and before the envelope check, the key, and any
35
+ worktree, so a bundle installed the wrong way (as someone else's dependency, never `npm
36
+ install`ed inside its own directory) fails fast with a fix instead of crashing deep
37
+ inside a close stage with a bare `ERR_MODULE_NOT_FOUND`.
38
+ - **`close-absolute-path` export guard** (`src/bundle.js`, F129): `exportBundle` now refuses
39
+ a close script whose source bakes in a string literal that is an absolute POSIX path
40
+ which exists on the exporting machine and is outside an allow-listed system prefix — a
41
+ close judges the `cwd` the runner gives it, never a path baked into its own source. Pins
42
+ the live defect where a hardcoded `WORKDIR` made the close judge the original patient
43
+ checkout instead of a fresh worktree, minting `already-green` at $0.
44
+ - **The aurora close now reads `process.cwd()`** (`scripts/u-spawner-close.mjs`) instead of a
45
+ hardcoded absolute `WORKDIR`, so it judges the tree the runner actually hands it rather than
46
+ the original patient checkout — the fix behind F129, validated live by a fourth paid fire
47
+ that went green from a fresh detached worktree (`docs/logs/FINDINGS.md`, F130's closing
48
+ pointer).
49
+
50
+ ### Fixed
51
+
52
+ - **F128 — `bareloop run` exit code now reflects the run outcome.** Previously every
53
+ invocation that reached the tail exited `0` regardless of outcome — a close-red, plan-
54
+ red, escalated, cap/wall-halted, or crashed run all read as CLI success. `run` now
55
+ returns `0` only for `green`/`already-green`; every other outcome exits `1`, so a caller
56
+ scripting off the exit code can no longer mistake a red run for a green one. The tail's
57
+ printed lines are unchanged.
58
+ - **F128 — the "minting run" line named the wrong run.** On a bundle's first (unblessed)
59
+ run, the notice printed the bridge's FIRST history row's `runid`, not the run that
60
+ actually minted the bundle's own signed spec. It now searches every bridge version
61
+ across ALL shipped bridges (base + shape fork) for the one whose `specHash` equals the
62
+ resolved `$BARELOOP_BUNDLE` spec's `jobSpecHash`, and prints "no version at this hash"
63
+ (honestly, never a guess) when none matches — which cross-machine is the common case,
64
+ since the historic mint's real script path and the importer's own bundle path rarely
65
+ coincide.
66
+ - **`bin/bareloop.mjs`: a closed stdout reader (`| head`) no longer crashes with an
67
+ unhandled `EPIPE`.** `process.stdout` now has an `'error'` listener that exits `0` on
68
+ `EPIPE` (the one legitimate `process.exit()` in this file — the reader is gone, so
69
+ there is no queued output left to lose) and re-throws anything else.
70
+ - **`scripts/replay-row.mjs`** — a $0 registry-row replay from an archived green spine: mints
71
+ the row a real run would have written via `writeRunGreenRow` had it been launched with
72
+ `--registry`, using the same seam and the same input derivation `scripts/run-u.mjs` uses.
73
+ Given it a permanent home this session (was scratchpad-only).
74
+
75
+ ## [0.20.0] — 2026-09-05
76
+
77
+ ### Added
78
+
79
+ - Operator-only scout OFF switch (`docs/product/SCOUT-CONTRAST.md`): `runJob`/`runPlan`
80
+ accept `scout` (boolean, default `true`); `false` on a fresh run skips the read-only
81
+ survey and records `scout-skipped { reason: 'operator-off' }` instead — the planner
82
+ drafts from `(no scout notes)`. `scripts/run-u.mjs --scout on|off` is the runner
83
+ surface, modelled on `--read-shim`; a non-boolean/unrecognised value throws/exits
84
+ before a token is spent. Runner territory — the signed spec hash is unaffected.
85
+ Measured (F126): both arms green on `aurora-u-spawner-types`, OFF cost $0.37 more
86
+ (inside the $0.40 noise band) — the scout is not dead weight; the switch stays as a
87
+ probe knob, no default flip.
88
+
89
+ ### Docs
90
+
91
+ - Export scoping and lock: `docs/product/EXPORT-SCOPING.md` (tree facts, proposed
92
+ bundle shape) and `docs/product/EXPORT-BUILD.md` (frozen build spec — worktree,
93
+ no-resign-on-tighten) record hamr's interview answers ahead of the export build
94
+ itself; no code shipped yet.
95
+ - G4 counting-rule scoping (`docs/product/G4-SCOPING.md`) and the first `docs/logs/BENCH.md`
96
+ bench-ledger row.
97
+
8
98
  ## [0.19.1] — 2026-09-02
9
99
 
10
100
  ### Fixed
package/README.md CHANGED
@@ -133,6 +133,20 @@ works, **export the whole workflow** — with its self-healing harness — and r
133
133
  anywhere. The point is to stop trial-and-erroring your way to the workflow that fits a job.
134
134
  Find it once, keep it, reuse it. It is a seed of something bigger over time.
135
135
 
136
+ ### Export a job
137
+
138
+ ```bash
139
+ bareloop export jobs/my-maintainer.json --registry ./bridges --out ./my-maintainer.bareloop
140
+ bareloop run ./my-maintainer.bareloop --repo /path/to/checkout --approve <bundleHash printed above>
141
+ ```
142
+
143
+ `export` only ships a job that has already earned a real green — it refuses to bundle an
144
+ unproven draft. The first `run` on a new machine asks you to approve the bundle's printed
145
+ hash; once that run goes green, later runs need no re-approval as long as the bundle stays
146
+ unchanged. Every run works in its own fresh git worktree and leaves the merge to you. Full
147
+ contract — flags, approval/blessing rules, exit codes — in
148
+ [`bareloop.context.md`](bareloop.context.md).
149
+
136
150
  ## Running it
137
151
 
138
152
  bareloop runs locally, as you. Secrets load from the environment and never enter the tree or
@@ -958,7 +958,7 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
958
958
  `coordination-red` — a failure between units (scope contention, step order, store
959
959
  races), never to be folded into worker/interpreter reds.
960
960
 
961
- ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, readShim?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun? })` → outcome — `src/run.js`
961
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, readShim?, scout?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun? })` → outcome — `src/run.js`
962
962
 
963
963
  The last seven are the RESUME fold and are documented under *Resuming a killed run* below; they
964
964
  default to `0` / `true` / `0` / `null` / `[]` / `null` / `null`, so a fresh run passes none of them.
@@ -1027,6 +1027,20 @@ arm replaces the CLI-display cap rather than stacking on it; a non-capping arm l
1027
1027
  installed *inside* the shim, so native never loses its bound. Default-flip pending a paid contrast;
1028
1028
  see CHANGELOG for the replay numbers behind it.
1029
1029
 
1030
+ **`scout` (default `true`) — the operator-only off switch** (`docs/product/SCOUT-CONTRAST.md`),
1031
+ threaded verbatim from `runJob` to `runPlan`. `false` on a FRESH run (no `resumeSeed`) skips the
1032
+ read-only survey entirely — `scout-start`/`scout-result`/`scout-empty` never fire, and a single
1033
+ `scout-skipped { reason: 'operator-off', meaning }` records it (a RECORD, never silence — the
1034
+ resume skip's own rule). The planner then drafts from `(no scout notes)` exactly as it does today
1035
+ when a scout returns an empty blob; nothing else observable changes. Precedence when both a
1036
+ resumed leg and `scout:false` are in play: the resume skip fires first and wins its own reason
1037
+ (`resumed`) — `operator-off` is only ever emitted on a fresh run. A non-boolean value **throws**
1038
+ a `TypeError` at the same door as the `readShim` arm guard, before a token is spent. It is a probe
1039
+ knob, not a product default — the spec names no scout, so the signed hash is unaffected — and
1040
+ `scripts/run-u.mjs --scout on|off` (default `on`) is its runner-territory surface, modelled on
1041
+ `--read-shim`: an unrecognised value exits 2 at argv, and every re-invocation the runner prints
1042
+ carries `--scout off` when set, so a resume never silently drops the arm.
1043
+
1030
1044
  **The two hitl terminals (N4 slice 1, doors re-cut 2026-08-18)** are the class's whole surface
1031
1045
  at this layer, and each is a CLEAN exit (`spendComplete` stays true — only the two casualties floor). `hitl-pause` is a
1032
1046
  decision-ready CHECKPOINT: the close reached a stage no machine can render, so the run stops
@@ -2497,6 +2511,221 @@ An unresolvable `--range` (no such ref — a fresh clone with no `origin`, a det
2497
2511
  checkout that never fetched) is a **skip**, printed and exited 0: a missing baseline is
2498
2512
  not a rule violation, never a red.
2499
2513
 
2514
+ ### Bundles — export a job, run it elsewhere (`src/bundle.js`, `src/cli.js`, `bin/bareloop.mjs`)
2515
+
2516
+ `docs/product/EXPORT-BUILD.md` (frozen build spec, 2026-09-05). A **bundle** is a plain
2517
+ directory that ships a proven job — spec, close scripts, and registry history — so it can
2518
+ run on a different machine without this repo. `npm pack` is its shipping form (there is no
2519
+ separate tarball step).
2520
+
2521
+ ```
2522
+ <job>.bareloop/
2523
+ manifest.json { schema:"bundle-v1", job, exportedAt, bareloopVersion, files:{...}, bundleHash }
2524
+ spec.json the job spec; close[].cmd rewritten to "node $BARELOOP_BUNDLE/close/<script> …"
2525
+ close/<script>.mjs the close script(s), verbatim except `from '../src/…'` → `from 'bareloop'`
2526
+ bridges/*.json the job's whole registry history for this job name (greens and reds)
2527
+ README.md the operator questions, in the order `bareloop run` asks them
2528
+ blessing.json absent at export; written by the run that first greens it
2529
+ history.jsonl absent at export; one line appended per `bareloop run` on this machine
2530
+ runs/<runid>/ absent at export; one per run — that run's own spine.jsonl + gate-audit.jsonl
2531
+ ```
2532
+
2533
+ **`bundleHash`** = sha256 over the sorted `path:contentSha256` lines of exactly `spec.json`
2534
+ and every `close/*` file (`hashFilesMap` in `src/bundle.js`) — nothing else. `manifest.json`,
2535
+ `README.md`, `bridges/*`, `blessing.json`, `history.jsonl` and `runs/` are all **outside**
2536
+ the hash on purpose: they are records, not the arbiter. This is the one signature a human
2537
+ approves and the CLI stores.
2538
+
2539
+ **Code-vs-spec, verified by reading `src/bundle.js`/`src/cli.js`:** `exportBundle` DOES write
2540
+ a `package.json` (name/version/`dependencies: {"bareloop": "^…"}`, `private: true`, `type:
2541
+ "module"`), outside `bundleHash` on purpose (editing it never trips `bundle-tampered`) — this
2542
+ corrects an earlier, now-stale reading of this section that claimed no `package.json` was
2543
+ written; `tests/bundle.test.js` proves both the write and the hash-exclusion directly. The
2544
+ `runs/<runid>/` directory (this run's relocated `spine.jsonl`/`gate-audit.jsonl`,
2545
+ written by `bareloop run`, step 6 below) is real and live but is not in the frozen spec's
2546
+ layout table at all. `history.jsonl`'s row also carries `bundleHash` and `approveHash`
2547
+ (the POC-fact correction, below) beyond the fields the original layout table named.
2548
+
2549
+ #### `src/bundle.js` — pure, no provider/process calls
2550
+
2551
+ | function | args → returns | notes |
2552
+ |---|---|---|
2553
+ | `exportBundle(o)` | `{ spec, closeScripts, registryDir, outDir, bareloopVersion }` → `{ ok, reds, dir, bundleHash, manifest }` | mints the directory above. Every check runs before anything touches disk — a red means nothing was written |
2554
+ | `bundleHash(dir)` | `dir` → sha256 hex | recomputes from what is on disk right now; throws only if `dir` itself is not a directory |
2555
+ | `readBundle(dir)` | `dir` → `{ ok, reds, spec, manifest, bridges, blessing, history }` | never throws; every failure is a typed red. Runs the tamper check (below) whenever both a manifest and a spec parsed |
2556
+ | `resolveBundleSpec(bundle, bundleDir)` | a `readBundle` result + the bundle dir → `{ spec, approveHash }` | the in-memory `$BARELOOP_BUNDLE` → absolute `bundleDir` substitution; `approveHash` is `jobSpecHash` of the *substituted* spec, never the unresolved one |
2557
+ | `checkEnvelope(spec, envelope)` | `spec` (already resolved) + `{ budgetUsd?, maxWallMs? }` → `{ ok, reds }` | tighten-only: a given number must be ≤ the spec's own |
2558
+ | `bless(dir, record)` | `dir` + `{ bundleHash, runid, outcome, host?, blessedAt? }` → the blessing object | writes/overwrites `blessing.json`; there is only ever one, for the bundle's current hash |
2559
+ | `verifyBlessing(bundle)` | a `readBundle` result → `{ ok, unblessed, reds }` | `unblessed: true` (no red) means never blessed here; a red means blessed-but-stale |
2560
+ | `appendHistory(dir, row)` | `dir` + a plain object → `void` | atomic append to `history.jsonl` (`appendFileSync`, same convention as `spine.js`/`ledger.js`) |
2561
+
2562
+ **Every red code `src/bundle.js` can emit, and when:**
2563
+
2564
+ | code | fires in | meaning |
2565
+ |---|---|---|
2566
+ | `invalid-value` | `exportBundle` (`spec.job` missing, `spec.close` empty/absent, `outDir` exists and is not a directory), `checkEnvelope` (a given `budgetUsd`/`maxWallMs` is not a positive finite number) | a shape the caller must fix before minting/tightening anything |
2567
+ | `outdir-not-empty` | `exportBundle` | `outDir` exists and already holds files — export never merges into a prior bundle |
2568
+ | `registry-unreadable` | `exportBundle` | `registryDir` itself could not be read (bubbled from `loadRegistry`) |
2569
+ | `no-bridge-at-hash` | `exportBundle` | no bridge named `spec.job` in `registryDir` carries a green (bridge-level or on any stored version) at `jobSpecHash(spec)` — export ships a run that actually happened, never an unproven draft |
2570
+ | `close-cmd-unrelocatable` | `exportBundle` | a `close[].cmd` is not exactly `node <absolute path>.mjs …` — the one shape this module can relocate under `$BARELOOP_BUNDLE` |
2571
+ | `close-script-missing` | `exportBundle` | a stage's `cmd` names a script `closeScripts` carries no source for |
2572
+ | `close-script-collision` | `exportBundle` | two different close script paths share one basename — a flat `close/` directory would silently clobber one |
2573
+ | `close-import-unparsed` | `exportBundle` | a close script's import is a shape this module cannot verify at all — default import, `* as ns`, mixed default+named/namespace, a bare side-effect import, a dynamic `import()`, or a relative import that does not point into `src/` (a sibling file never ships in `close/`). Fails safe rather than silently accepting what it cannot read |
2574
+ | `close-import-unexported` | `exportBundle` | a close script imports a name from bareloop's own `src` that `src/index.js` does not export |
2575
+ | `close-absolute-path` | `exportBundle` (F129) | a close script's source bakes in a quoted string literal that is an absolute POSIX path which `existsSync` finds real on the exporting machine and which is not under an allow-listed system prefix (`/usr/`, `/bin/`, `/sbin/`, `/lib/`, `/lib64/`, `/dev/`, `/etc/`, `/proc/`, `/sys/`, `/opt/`). **A close judges `process.cwd()` — the runner's cwd — never a path baked into the script (F8/F129):** the live defect this catches is a hardcoded `WORKDIR` making a close judge the ORIGINAL patient checkout instead of the fresh worktree it was actually pointed at, minting a fake `already-green` at $0. A nonexistent absolute-looking literal is a NAME, not a proven hazard, and is not flagged — the check is deliberately monotone and simple, not clever (see `absolutePathLiteralsOf`'s JSDoc for its named limits: string concatenation and cross-machine-only paths are invisible to it) |
2576
+ | `bundle-missing` | `readBundle` | `dir` is not a directory at all |
2577
+ | `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
2578
+ | `bundle-tampered` | `readBundle` | manifest and spec both parsed, but the recomputed `bundleHash` (from what's really on disk) does not match the stored one — **the load-bearing check, N4 below** |
2579
+ | `bundle-deps-missing` | `checkBundleDeps` (F128) | the bundle's own `close/` directory cannot resolve `require.resolve('bareloop')` — the bundle was installed the WRONG way (as someone else's dependency) instead of `cd <bundleDir> && npm install`. `bareloop run` calls this as literal step 1b, right after the tamper check, before the envelope check, the key, and any worktree |
2580
+ | `envelope-widen` | `checkEnvelope` | a given `budgetUsd`/`maxWallMs` exceeds the bundle's own signed ceiling — raising one is a re-export, never a runner flag |
2581
+ | `blessing-stale` | `verifyBlessing` | `blessing.json` exists but its `bundleHash` no longer matches the manifest's — the bundle was re-exported/edited since the run that blessed it; re-export and re-approve |
2582
+
2583
+ **N4 — why `readBundle`'s tamper check must run before anything else touches the bundle.**
2584
+ `jobSpecHash` covers `close[].cmd` as a *path*, not the script's bytes, so a swapped close
2585
+ script leaves the signed spec's own hash intact. `runJob`'s close-first precheck
2586
+ (`src/planrun.js`) would then read the swapped script's fake "already green" as a genuine
2587
+ one — a fake green, $0 spent, no work done, reported as success. `readBundle`'s
2588
+ `bundle-tampered` red is therefore not a courtesy check: it is the only thing standing
2589
+ between a swapped script and a fake green, so `bareloop run` calls it as literal step 1,
2590
+ before the envelope check, before the key check, before the worktree, before `runJob` is
2591
+ ever reached.
2592
+
2593
+ #### The CLI — `bareloop export | run | history`, `bin/bareloop.mjs`
2594
+
2595
+ `bin/bareloop.mjs` is a ~10-line adapter: it supplies the real `deps` (real `env`/`stdout`/
2596
+ `stderr`/`cwd`/`stdin`) and turns the returned number into `process.exitCode` — never
2597
+ `process.exit()`, which can discard queued stdout under a slow reader. All logic lives in
2598
+ `src/cli.js`'s `main(argv, deps)`, exported from the package root as **`cliMain`**
2599
+ (`export { main as cliMain } from './cli.js'` — renamed on export so it never collides with
2600
+ some other module's own generic `main`). `cliMain(argv, deps)` is the in-process test/
2601
+ integration seam: pass `deps.provider` (and optionally `providerFor`/`judgeProvider`) and the
2602
+ real `ANTHROPIC_API_KEY` check is skipped entirely — a scripted provider *is* the run, the
2603
+ same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
2604
+ `now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
2605
+
2606
+ Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
2607
+ quit` — and asks each sub-command's arguments one line at a time over the SAME code paths
2608
+ below.
2609
+
2610
+ - **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
2611
+ close-script paths against the spec file's own directory (never the process cwd), calls
2612
+ `exportBundle`. On success prints `bundleHash:`, the sorted file list, and "this bundle is
2613
+ unblessed until its first run greens on the importer's machine." Exit `1` on any red or
2614
+ missing flag, `0` on success.
2615
+
2616
+ - **`bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]`**
2617
+ — the exact order below; no step is ever reordered:
2618
+ 1. `readBundle(bundleDir)` — the tamper check (N4, above). Any red stops here, exit `1`.
2619
+ 2. `checkBundleDeps(bundleDir)` (F128) — can the bundle's own `close/` directory resolve
2620
+ `require.resolve('bareloop')`? A bundle installed the WRONG way (as someone else's
2621
+ dependency — see the corrected adopter flow below) has no `node_modules` of its own,
2622
+ which would otherwise crash the close deep inside the precheck with a bare
2623
+ `ERR_MODULE_NOT_FOUND`. A red here (`bundle-deps-missing`) prints the exact cure line
2624
+ (`cd <bundleDir> && npm install`) and stops, exit `1`, before the envelope check, the
2625
+ key, or any worktree.
2626
+ 3. `checkEnvelope(spec, { budgetUsd?, maxWallMs? })` — `--wall` is minutes, converted to ms.
2627
+ A red (an invalid number, or a widen) stops here, exit `1`.
2628
+ 4. The provider key: `deps.provider` if the caller injected one (the test seam), else
2629
+ `ANTHROPIC_API_KEY` from `env`. Absent and no injected provider: print the bundle's own
2630
+ `README.md` plus `bundleHash:`, spend nothing, **exit `0`** (this is a legitimate,
2631
+ non-error stop, not a red).
2632
+ 5. Blessing. No `blessing.json` yet: print the "first run" notice + the minting-run line
2633
+ (F128-corrected — see below), the `bundleHash`, and **require** `--approve <bundleHash>`
2634
+ to match exactly, or exit `1`. A `blessing.json` present: `verifyBlessing` must pass
2635
+ (`blessing-stale` stops here, exit `1`); a `--approve` flag is accepted but ignored with
2636
+ a printed note ("no-resign" — hamr's ruling).
2637
+ 6. The worktree: refuse (exit `1`) if `--repo` is not a git repo with ≥1 commit, or if
2638
+ `<repo>/.bareloop/wt/<runid>` already exists; otherwise `git worktree add --detach
2639
+ <repo>/.bareloop/wt/<runid> HEAD` — **always fresh, never reused** (a reused worktree
2640
+ would read a prior run's edits as "already-green" — a negative POC's exact finding).
2641
+ 7. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/
2642
+ `maxWallMs` on that resolved spec in memory if `--budget`/`--wall` were given → mint a
2643
+ **fresh** `approveHash = jobSpecHash(that tightened+resolved spec)` (the runner signs
2644
+ the spec it is actually about to run; the human-approved `bundleHash` never changes) →
2645
+ `runJob(runSpec, { approvals: [{ specHash: approveHash, signer: 'bundle', ts }], workdir:
2646
+ worktree, provider, providerFor, judgeProvider, emit, shellCapUsd: runSpec.budgetUsd,
2647
+ readShim: 'cap', scout: true })`, spine written to `<bundleDir>/runs/<runid>/spine.jsonl`.
2648
+ A thrown `runJob` crash is caught and reported, exit `1`.
2649
+ 8. The worktree's `gate-audit.jsonl` is moved to `<bundleDir>/runs/<runid>/gate-audit.jsonl`
2650
+ (same relocation `scripts/run-u.mjs` does for in-repo runs). `spentUsd`/`spendComplete`
2651
+ are read off this run's own `job-end` spine event — never fabricated as `0` when unknown.
2652
+ 9. `appendHistory` — one `history.jsonl` line: `{ runid, at, outcome, spentUsd,
2653
+ spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash, approveHash }` (the
2654
+ `bundleHash` ↔ `approveHash` pairing, POC fact 2, so the human-signed hash and the
2655
+ hash actually enforced can never drift apart silently). A `green` outcome on a still-
2656
+ unblessed bundle also calls `bless(bundleDir, { bundleHash, runid, outcome, host })`.
2657
+ 10. The tail: `outcome`, `spent` (`≥$…` when `spendComplete === false`, `UNKNOWN` when
2658
+ `spentUsd` is `null`), `branch`, `worktree`, a `git merge <branch>` instruction
2659
+ ("merge stays human — this CLI never merges"), and "the worktree is kept until you
2660
+ remove it: `git worktree remove <worktree>`" (F110: never delete it for the user).
2661
+ **Exit `0` ONLY for `green`/`already-green` (F128) — every other outcome (close-red,
2662
+ plan-red, escalated, cap/wall-halted, provider-red, a crashed `runJob`, …) exits `1`,**
2663
+ so a caller scripting off the exit code can never mistake a red run for a green one.
2664
+ The tail's printed lines are unchanged either way.
2665
+
2666
+ **Adopter flow, corrected (F128, 2026-09-06 — hamr's paid fire found the frozen spec's
2667
+ validation step 2 named the wrong install shape):** `npm install <bundle dir>` run from a
2668
+ clean CONSUMER directory installs the bundle AS that consumer's own dependency — it never
2669
+ installs the bundle's OWN declared dependency (`package.json`'s `bareloop`), which its
2670
+ close scripts need at run time. The correct flow is **`cd <bundle> && npm install`** (reads
2671
+ the bundle's own `package.json`), then **`./node_modules/.bin/bareloop run . --repo <path>
2672
+ …`**. `checkBundleDeps` (above) is the fail-safe that catches the wrong-shape install
2673
+ before any close ever runs; see `docs/logs/FINDINGS.md` F128 for the full account
2674
+ (grounded in the real crashed spine) and `docs/product/EXPORT-BUILD.md`'s dated addendum.
2675
+
2676
+ **Minting-run line, corrected (F128).** The first-run notice used to print the shipped
2677
+ bridge's FIRST history row's `runid`, unconditionally. It now searches every bridge
2678
+ VERSION across all shipped bridges (base + shape fork) for the one whose `specHash` equals
2679
+ the resolved `$BARELOOP_BUNDLE` spec's `jobSpecHash`, printing `minting run: <runid> (spine
2680
+ not bundled in v1)` on a match or **`no version at this hash`** (never a guessed runid)
2681
+ when none matches — which, cross-machine, is the common case, since the historic mint's
2682
+ real close-script path and the importer's own bundle path essentially never coincide.
2683
+
2684
+ - **`bareloop history <bundleDir>`** → prints every `history.jsonl` line verbatim, then
2685
+ every bridge's `listingRow(b)` from `<bundleDir>/bridges`. Always exit `0`; "(no runs
2686
+ yet)" when `history.jsonl` doesn't exist.
2687
+
2688
+ **Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
2689
+ bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
2690
+ anything higher. Raising a ceiling is a re-export (a new signed bundle), never a runner flag.
2691
+
2692
+ **First-run approval, blessing, no-resign.** The bundle's own signed `bundleHash` is what a
2693
+ human reads and accepts with `--approve` on the run that first blesses it. That first GREEN
2694
+ writes `blessing.json`; every later run on that machine, against that same unchanged bundle,
2695
+ needs no `--approve` at all ("no-resign", hamr's ruling) — it only has to pass
2696
+ `verifyBlessing`. Editing the bundle after blessing (or re-exporting it) changes
2697
+ `manifest.bundleHash`, so the stored blessing goes stale (`blessing-stale`) and the human
2698
+ has to approve again.
2699
+
2700
+ **Fresh worktree per run; merge stays human.** Every `bareloop run` creates a brand-new
2701
+ `git worktree add --detach <repo>/.bareloop/wt/<runid>` off `HEAD` and hands that path to
2702
+ `runJob` as `workdir` — the caller's own checkout is never touched, and no worktree is ever
2703
+ reused (reuse would let a prior run's uncommitted edits masquerade as this run's own green).
2704
+ The CLI never merges anything itself: the tail prints the exact `git merge <branch>` command
2705
+ and leaves the worktree in place until the human removes it.
2706
+
2707
+ **`history.jsonl` row shape** — one JSON object per line, one line per `bareloop run` on this
2708
+ machine: `{ runid, at, outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree,
2709
+ branch, bundleHash, approveHash }`. `bundleHash` is the bundle's own signed manifest hash
2710
+ (unchanged across tightened runs); `approveHash` is the hash of the actual, possibly-
2711
+ tightened, `$BARELOOP_BUNDLE`-resolved spec that run executed — the two are recorded side
2712
+ by side so a tightened run's real approval can never be confused with the bundle's headline
2713
+ signature.
2714
+
2715
+ **Vocabulary:** `close-red` means the close **instrument** itself broke — it could not run,
2716
+ never finished, was killed, or crashed without judging anything (`CLOSE_FAULTS`, above) —
2717
+ never a judgment about the work. `plan-red` means the close ran, judged, and rendered a real
2718
+ **no**. A `readBundle`/`checkEnvelope`/`checkBundleDeps` red is a bundle-shape, envelope, or
2719
+ dependency refusal (before any provider call); it is none of those three — call it what it
2720
+ is: a bundle-red, an envelope-red, or a deps-red, not a close verdict. (F128's live defect
2721
+ would, unfixed, have shown up as `close-red` at runtime for a reason that had nothing to do
2722
+ with the close itself — `checkBundleDeps` exists precisely so a wrong-shape install never
2723
+ reaches that far.)
2724
+
2725
+ **What is NOT in v1 (hamr Q7):** the review door at the run's tail, `soft-green`, any
2726
+ provider but `anthropic-api`, spine bundling for `formatReplay` on the minting run (only its
2727
+ `runid` is noted), and tarball packing — a bundle is a plain directory; use `npm pack` on it
2728
+ yourself if you want a tarball.
2500
2729
 
2501
2730
  ## Architecture
2502
2731
 
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Export M2 — a thin adapter. ALL logic lives in `src/cli.js`; this file only
3
+ // supplies the real deps and turns the returned exit code into
4
+ // `process.exitCode` (never `process.exit()`, which can discard queued
5
+ // stdout under a slow reader).
6
+ import { main } from '../src/cli.js';
7
+
8
+ // A reader closing early (e.g. `| head`) makes a queued stdout write throw
9
+ // EPIPE — the one legitimate `process.exit()` in this file: the reader is
10
+ // gone, so there is no more queued output to lose by exiting immediately,
11
+ // and re-throwing would print a scary stack trace for ordinary pipe usage.
12
+ process.stdout.on('error', (e) => {
13
+ if (e.code === 'EPIPE') process.exit(0);
14
+ throw e;
15
+ });
16
+
17
+ const code = await main(process.argv.slice(2), {
18
+ env: process.env,
19
+ stdout: process.stdout,
20
+ stderr: process.stderr,
21
+ cwd: process.cwd(),
22
+ stdin: process.stdin,
23
+ });
24
+ process.exitCode = code;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.19.1",
4
- "description": "Workflows that earn their own design, with receipts \u2014 agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
3
+ "version": "0.21.0",
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",
7
7
  "types": "./types/index.d.ts",
@@ -11,6 +11,9 @@
11
11
  "default": "./src/index.js"
12
12
  }
13
13
  },
14
+ "bin": {
15
+ "bareloop": "bin/bareloop.mjs"
16
+ },
14
17
  "license": "Apache-2.0",
15
18
  "author": "hamr0",
16
19
  "engines": {
@@ -38,6 +41,7 @@
38
41
  "homepage": "https://github.com/hamr0/bareloop#readme",
39
42
  "files": [
40
43
  "src/",
44
+ "bin/",
41
45
  "types/",
42
46
  "NOTICE",
43
47
  "bareloop.context.md",