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 +90 -0
- package/README.md +14 -0
- package/bareloop.context.md +230 -1
- package/bin/bareloop.mjs +24 -0
- package/package.json +6 -2
- package/src/bundle.js +719 -0
- package/src/cli.js +460 -0
- package/src/index.js +17 -1
- package/src/planrun.js +26 -1
- package/src/reuse.js +1 -1
- package/src/run.js +12 -2
- package/types/bundle.d.ts +199 -0
- package/types/cli.d.ts +19 -0
- package/types/index.d.ts +3 -1
- package/types/planrun.d.ts +13 -1
- package/types/reuse.d.ts +14 -0
- package/types/run.d.ts +8 -1
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
|
package/bareloop.context.md
CHANGED
|
@@ -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
|
|
package/bin/bareloop.mjs
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Workflows that earn their own design, with receipts
|
|
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",
|