bareloop 0.20.0 → 0.22.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 +234 -0
- package/README.md +14 -0
- package/bareloop.context.md +331 -10
- package/bin/bareloop.mjs +24 -0
- package/package.json +7 -3
- package/src/authorjob.js +15 -1
- package/src/bundle.js +713 -0
- package/src/cli.js +472 -0
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/index.js +31 -1
- package/src/job.js +48 -4
- package/src/plan.js +5 -1
- package/src/planrun.js +212 -7
- package/src/ralph.js +40 -7
- package/src/reuse.js +4 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +24 -3
- package/src/validate.js +59 -0
- package/types/bundle.d.ts +199 -0
- package/types/cli.d.ts +19 -0
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/index.d.ts +4 -1
- package/types/planrun.d.ts +17 -2
- package/types/ralph.d.ts +19 -5
- package/types/reuse.d.ts +16 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +25 -2
- package/types/validate.d.ts +48 -0
package/bareloop.context.md
CHANGED
|
@@ -134,7 +134,8 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
134
134
|
| `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
|
|
135
135
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
136
136
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
137
|
-
| `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage number, so a spec with many stages can validate while its overshoot dwarfs its cap:
|
|
137
|
+
| `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
|
|
138
|
+
| `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
|
|
138
139
|
| `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the judge model stays library-pinned (`JUDGE_MODEL`) pending recalibration |
|
|
139
140
|
| `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
|
|
140
141
|
| `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
|
|
@@ -391,7 +392,7 @@ Append-only JSONL event emitter bound to one file. `seq` monotonic per spine, `t
|
|
|
391
392
|
last. Consumers are pure listeners; nothing reads the file back. Returns each event as
|
|
392
393
|
written.
|
|
393
394
|
|
|
394
|
-
### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
395
|
+
### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites?, closeDir? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
395
396
|
|
|
396
397
|
The dumb outer shell: `while close-red and not exhausted: run the middle`. **Exhaustion has
|
|
397
398
|
two alternative rules and you supply exactly one** — `capRuns` (a fixed number of middle runs)
|
|
@@ -422,11 +423,23 @@ set the close's `gapKeep` (job-spec, arbiter territory) to a regex like `"^not o
|
|
|
422
423
|
matching failure lines are preserved in a capped kept-failures block regardless of where
|
|
423
424
|
they print (F28: the first real firing delivered a gap with zero failure names). The gap
|
|
424
425
|
also combines stdout+stderr, so a stdout failure survives stderr noise.
|
|
425
|
-
`closeTimeoutMs` caps the close's wall clock (default 120s
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
`closeTimeoutMs` caps the close's wall clock (default 120s when nothing else supplies a
|
|
427
|
+
value — `runClose`'s own bare default) — shell/operator territory, inexpressible in any
|
|
428
|
+
config. A faulted close is asked to leave with SIGTERM and, if it is still there 2s later,
|
|
429
|
+
ended with SIGKILL: SIGTERM is a request a close can decline (its own handler, a trapping
|
|
430
|
+
wrapper), and without a second deadline the wait never resolves. The verdict is untouched —
|
|
431
|
+
the FIRST fault named the outcome and the kill only enforces it. **As of PRD item 27/M3, a
|
|
432
|
+
direct `ralph`/`runClose`/`runStages` caller still owns this number outright (`closeTimeoutMs
|
|
433
|
+
?? 120_000`); the AUTOSET/override resolution lives one level up, in `runPlan`/`runJob`
|
|
434
|
+
(see `resolveCloseTimeoutMs` below) — `ralph` itself never runs a timing pass.**
|
|
435
|
+
|
|
436
|
+
`closeDir` (PRD item 27/M3 Part B, optional) is the close's own BOOKS DIRECTORY — pristine
|
|
437
|
+
copies, calibration thresholds, logs — deliberately outside the patient tree (the worker
|
|
438
|
+
must never read the arbiter's books, v1.12). Forwarded to every close invocation as the
|
|
439
|
+
env var `BARELOOP_CLOSE_DIR` (set on a COPY of `process.env`, after `CLOSE_ENV_DENY`
|
|
440
|
+
stripping — a `BARELOOP_` name matches none of that denylist's rules and is never stripped).
|
|
441
|
+
A close script that reads this variable and gets none must instrument-stop itself; nothing
|
|
442
|
+
in the runner invents a fallback directory.
|
|
430
443
|
|
|
431
444
|
**The close child's environment is stripped of credential-shaped variables before it runs**
|
|
432
445
|
(`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`, any name ending
|
|
@@ -958,13 +971,33 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
|
|
|
958
971
|
`coordination-red` — a failure between units (scope contention, step order, store
|
|
959
972
|
races), never to be folded into worker/interpreter reds.
|
|
960
973
|
|
|
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`
|
|
974
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
|
|
975
|
+
|
|
976
|
+
**`closeTimeoutMs` (PRD item 27/M3) is now OPTIONAL for every real caller.** Omit it (both
|
|
977
|
+
`src/cli.js`'s bundle runner and `scripts/run-u.mjs` do) and the run AUTOSETS the per-stage
|
|
978
|
+
close ceiling from a $0 seed timing pass, or reads the job spec's own signed override —
|
|
979
|
+
"guesstimate + loud sign + customer override", the same shape rates-passthrough already
|
|
980
|
+
uses (F113). Passing an explicit value here still wins outright over both (kept for
|
|
981
|
+
backward compatibility and test control) — see `resolveCloseTimeoutMs` further down.
|
|
982
|
+
|
|
983
|
+
**`closeDir` (PRD item 27/M3 Part B, optional but effectively required for any
|
|
984
|
+
hand-authored close that reads its own books)** — the close's own directory (pristine
|
|
985
|
+
copies, calibration thresholds, logs), outside the patient tree. `src/cli.js` sets it to
|
|
986
|
+
`<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs` sets it to its existing `spineDir`. A
|
|
987
|
+
close script whose source mentions `BARELOOP_CLOSE_DIR` and gets none here refuses the run
|
|
988
|
+
at $0 (`close-dir-required`, before any provider call) — see `checkCloseDirRequired` below.
|
|
962
989
|
|
|
963
990
|
The last seven are the RESUME fold and are documented under *Resuming a killed run* below; they
|
|
964
991
|
default to `0` / `true` / `0` / `null` / `[]` / `null` / `null`, so a fresh run passes none of them.
|
|
965
992
|
Three of them are folds of a bound the operator SIGNED (money, wall, replans) and one is a
|
|
966
993
|
readout seed (grades) — the distinction matters and is spelled out there.
|
|
967
994
|
|
|
995
|
+
`resumable` (default `true`, PRD item 27(c)/F130) says whether THIS runner supports
|
|
996
|
+
`--resume` at all — `run-u.mjs` leaves it at the default (byte-identical to before this
|
|
997
|
+
flag existed); the exported bundle CLI (`src/cli.js`) passes `false` so the run's
|
|
998
|
+
escalation tail says "resume is `run-u`-only in v1" instead of naming a flag it does not
|
|
999
|
+
implement.
|
|
1000
|
+
|
|
968
1001
|
The runner — the shell's top layer, and the ONE entry. It composes everything below it and
|
|
969
1002
|
interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
|
|
970
1003
|
unapproved spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
|
|
@@ -1601,7 +1634,7 @@ package makes). Which runs open one is `doorOpens(job, reviewDoor)`:
|
|
|
1601
1634
|
territory.
|
|
1602
1635
|
|
|
1603
1636
|
**Answering one — `answerReviewDoor({ job, workdir, events, decision, text?, closeTimeoutMs?,
|
|
1604
|
-
registryDir?, name?, runid?, at?, now?, emit? })`** (`src/reviewdoor.js`). The answer arrives
|
|
1637
|
+
closeDir?, registryDir?, name?, runid?, at?, now?, emit? })`** (`src/reviewdoor.js`). The answer arrives
|
|
1605
1638
|
after the run has ended, possibly days later and from another process, so it cannot ride the
|
|
1606
1639
|
run's return path. This is that seam. It never throws and never conjures — a fourth door, an
|
|
1607
1640
|
empty `rerun`, a run that opened no door (`no-door`), an expired one (`door-expired`), a tree
|
|
@@ -2481,7 +2514,9 @@ nothing here to check it against).
|
|
|
2481
2514
|
|
|
2482
2515
|
Enforcement is **local only** — wired into `npm test` (see `package.json`'s `test`
|
|
2483
2516
|
script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
|
|
2484
|
-
enforced there without an ask-first CI edit).
|
|
2517
|
+
enforced there without an ask-first CI edit). `npm test` itself now runs `node --test`
|
|
2518
|
+
through `scripts/hermetic.mjs`, which strips this machine's `HOME`/git identity so a
|
|
2519
|
+
test that leans on either fails locally instead of only on CI (F136). The check itself lives outside the
|
|
2485
2520
|
published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
|
|
2486
2521
|
`scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
|
|
2487
2522
|
this ships):
|
|
@@ -2511,6 +2546,292 @@ An unresolvable `--range` (no such ref — a fresh clone with no `origin`, a det
|
|
|
2511
2546
|
checkout that never fetched) is a **skip**, printed and exited 0: a missing baseline is
|
|
2512
2547
|
not a rule violation, never a red.
|
|
2513
2548
|
|
|
2549
|
+
### Bundles — export a job, run it elsewhere (`src/bundle.js`, `src/cli.js`, `bin/bareloop.mjs`)
|
|
2550
|
+
|
|
2551
|
+
`docs/product/EXPORT-BUILD.md` (frozen build spec, 2026-09-05). A **bundle** is a plain
|
|
2552
|
+
directory that ships a proven job — spec, close scripts, and registry history — so it can
|
|
2553
|
+
run on a different machine without this repo. `npm pack` is its shipping form (there is no
|
|
2554
|
+
separate tarball step).
|
|
2555
|
+
|
|
2556
|
+
```
|
|
2557
|
+
<job>.bareloop/
|
|
2558
|
+
manifest.json { schema:"bundle-v1", job, exportedAt, bareloopVersion, files:{...}, bundleHash }
|
|
2559
|
+
spec.json the job spec; close[].cmd rewritten to "node $BARELOOP_BUNDLE/close/<script> …"
|
|
2560
|
+
close/<script>.mjs the close script(s), verbatim except `from '../src/…'` → `from 'bareloop'`
|
|
2561
|
+
bridges/*.json the job's whole registry history for this job name (greens and reds)
|
|
2562
|
+
README.md the operator questions, in the order `bareloop run` asks them
|
|
2563
|
+
blessing.json absent at export; written by the run that first greens it
|
|
2564
|
+
history.jsonl absent at export; one line appended per `bareloop run` on this machine
|
|
2565
|
+
runs/<runid>/ absent at export; one per run — that run's own spine.jsonl + gate-audit.jsonl
|
|
2566
|
+
```
|
|
2567
|
+
|
|
2568
|
+
**`bundleHash`** = sha256 over the sorted `path:contentSha256` lines of exactly `spec.json`
|
|
2569
|
+
and every `close/*` file (`hashFilesMap` in `src/bundle.js`) — nothing else. `manifest.json`,
|
|
2570
|
+
`README.md`, `bridges/*`, `blessing.json`, `history.jsonl` and `runs/` are all **outside**
|
|
2571
|
+
the hash on purpose: they are records, not the arbiter. This is the one signature a human
|
|
2572
|
+
approves and the CLI stores.
|
|
2573
|
+
|
|
2574
|
+
**Code-vs-spec, verified by reading `src/bundle.js`/`src/cli.js`:** `exportBundle` DOES write
|
|
2575
|
+
a `package.json` (name/version/`dependencies: {"bareloop": "^…"}`, `private: true`, `type:
|
|
2576
|
+
"module"`), outside `bundleHash` on purpose (editing it never trips `bundle-tampered`) — this
|
|
2577
|
+
corrects an earlier, now-stale reading of this section that claimed no `package.json` was
|
|
2578
|
+
written; `tests/bundle.test.js` proves both the write and the hash-exclusion directly. The
|
|
2579
|
+
`runs/<runid>/` directory (this run's relocated `spine.jsonl`/`gate-audit.jsonl`,
|
|
2580
|
+
written by `bareloop run`, step 6 below) is real and live but is not in the frozen spec's
|
|
2581
|
+
layout table at all. `history.jsonl`'s row also carries `bundleHash` and `approveHash`
|
|
2582
|
+
(the POC-fact correction, below) beyond the fields the original layout table named.
|
|
2583
|
+
|
|
2584
|
+
#### `src/close-integrity.js` — close-absolute-path (M1) AND close-bytes signature (M2)
|
|
2585
|
+
|
|
2586
|
+
Shared by `src/bundle.js` (export time) and a $0 run-start precheck inside `runPlan`
|
|
2587
|
+
(`src/planrun.js`) — every job, not only exported bundles, refuses at $0 (before the
|
|
2588
|
+
close-first precheck, before any provider call) when a close script's CONTENT bakes in an
|
|
2589
|
+
absolute path that exists on disk. Two spellings of the same rule is exactly how F129
|
|
2590
|
+
shipped an export-only guard while every non-exported job stayed exposed.
|
|
2591
|
+
|
|
2592
|
+
**Scope (widened 2026-09-06, PRD item 27/M2, an orchestrator audit):** a close script is
|
|
2593
|
+
addressable — visible to `readCloseScripts`, `checkCloseAbsolutePaths`, the sha256
|
|
2594
|
+
fingerprint, and `signCloseScripts` — when its `cmd` is EITHER an interpreter
|
|
2595
|
+
(`node`/`sh`/`bash`/`python`/`python3`/`npx`) followed by a non-empty argv[1], OR a bare
|
|
2596
|
+
cmd whose argv[0] ITSELF is an absolute path (a `.sh`/other directly-executable close
|
|
2597
|
+
script, no interpreter prefix — e.g. `jobs/aurora-testgen-cold.json`'s close). A relative
|
|
2598
|
+
bare executable (`true`, `pytest`, `npm test`) names no file at all and stays out of
|
|
2599
|
+
scope, on both halves, exactly as before. The shape test itself
|
|
2600
|
+
(`closeScriptCandidateToken`, `src/validate.js`) is shared with `src/job.js`'s sha256
|
|
2601
|
+
demand so the demand and the detectors can never drift apart about which stages are in
|
|
2602
|
+
scope.
|
|
2603
|
+
|
|
2604
|
+
| function | args → returns | notes |
|
|
2605
|
+
|---|---|---|
|
|
2606
|
+
| `absolutePathLiteralsOf(source)` | close script text → `string[]` | the scan itself (moved here from `src/bundle.js`, which now imports it) |
|
|
2607
|
+
| `readCloseScripts(spec, cwd)` | resolved job spec + cwd → `{ stage, path, bytes, sha256 }[]` | resolves every addressable `close[].cmd` (see scope above) to its absolute path and bytes off disk; a relative path resolves against `cwd`. Never throws — a missing/unreadable script reports `bytes: null`. `sha256` (M2, additive) is the stage's own signed field verbatim, or `null` |
|
|
2608
|
+
| `checkCloseAbsolutePaths(spec, cwd)` | resolved job spec + cwd → `{ ok: true } \| { ok: false, reds: { stage, path, literal }[] }` | the run-start check `runPlan` calls before the close-first precheck |
|
|
2609
|
+
| `hashCloseScriptBytes(text)` | script text → sha256 hex | the ONE formula every M2 consumer uses (minting, the run-start check, the runtime re-verify, the export mismatch check) — hashing the utf8-decoded text agrees with hashing the raw file bytes because a close script is always valid UTF-8 |
|
|
2610
|
+
| `checkCloseByteSignature(spec, cwd)` | resolved job spec + cwd → `{ ok: true } \| { ok: false, reds: { stage, path, expected, actual }[] }` | the $0 run-start integrity check (PRD item 27): every signed `close[].sha256` must match the script's bytes off disk RIGHT NOW. `runPlan` calls this alongside `checkCloseAbsolutePaths`, before the close-first precheck and before any provider call — a mismatch reds `close-tampered`. An unreadable signed script reads `actual: null` and is STILL a red (never silently "fine") |
|
|
2611
|
+
| `checkStageByteSignature(stages, cwd)` | a close stage chain + cwd → same shape as above | the narrower re-verify `runPlan`'s `runCloseStages` wrapper runs before EVERY close execution (precheck, preflight, check-passes, and the close-fix loop all go through this one seam) — a mismatch short-circuits to a `close-tampered` verdict in the same shape `runClose`/`runStages` render, so it rides out through the existing `CLOSE_FAULTS` machinery exactly like `crashed`/`timed-out`/`killed` |
|
|
2612
|
+
| `signCloseScripts(spec, cwd)` | resolved job spec + cwd → `{ ok, spec, changes: {stage,path,old,new}[], missing: {stage,path}[] }` | the PURE minting helper: fills `close[].sha256` from disk for every addressable stage. Never writes anything itself — `scripts/sign-close.mjs` is the thin CLI wrapper that gates on `--write`. A `closeDecl` spec (no script path to sign) returns `ok:false` unchanged |
|
|
2613
|
+
| `checkCloseDirRequired(spec, cwd, closeDir)` | resolved job spec + cwd + the runner's `closeDir` (or `null`) → `{ ok: true } \| { ok: false, reds: { stage, path }[] }` | PRD item 27/M3 Part B: scans the same addressable-script bytes `readCloseScripts` reads for the literal substring `BARELOOP_CLOSE_DIR` — a script that mentions it needs a real `closeDir` or it would run against `undefined`. A script that never mentions it carries no demand |
|
|
2614
|
+
| `CLOSE_DIR_ENV_VAR` | `'BARELOOP_CLOSE_DIR'` | the ONE spelling `checkCloseDirRequired`'s scan and `runClose`'s env-set (`src/ralph.js`) both use |
|
|
2615
|
+
|
|
2616
|
+
#### `src/closetimeout.js` — close timeout: autoset + signed override (M3)
|
|
2617
|
+
|
|
2618
|
+
hamr's ruling (2026-09-06): *"both … autoset and can be override, same like api pricing"*
|
|
2619
|
+
— the guesstimate-plus-loud-sign-plus-customer-override shape (F113), never a hidden
|
|
2620
|
+
defaulted knob. Arbiter constants, hamr 2026-09-07 verbatim: **floor = 120,000 ms; K = 5
|
|
2621
|
+
("2 and 5 are fine")** — never agent- or adopter-settable.
|
|
2622
|
+
|
|
2623
|
+
| export | shape | notes |
|
|
2624
|
+
|---|---|---|
|
|
2625
|
+
| `CLOSE_TIMEOUT_FLOOR_MS` | `120_000` | the floor, hamr's arbiter constant |
|
|
2626
|
+
| `CLOSE_TIMEOUT_K` | `5` | the multiplier, hamr's arbiter constant |
|
|
2627
|
+
| `LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS` | `120_000` | `runClose`'s own un-autoset default (`src/ralph.js`'s `timeoutMs = 120_000`) — named separately from the floor above even though the two numbers agree today, because they are different claims |
|
|
2628
|
+
| `TIMING_PREFLIGHT_CEILING_MS` | `600_000` (library default × K) | the GENEROUS provisional ceiling every stage runs under DURING the timing pass itself — never the autoset/override ceiling the run then uses, which is only known after the pass completes |
|
|
2629
|
+
| `timeCloseStages(stages, {cwd, redact})` | → `{ perStage: {name,ms,exitCode,timedOut}[], slowestMs, slowestName, anyTimedOut }` | runs EVERY stage once, ignoring verdicts (first-red-wins stays for the real verdict precheck) — a declared/kind stage (no `.cmd`) is skipped, contributing no reading rather than a false zero. A stage that never finishes within `TIMING_PREFLIGHT_CEILING_MS` reads `timedOut: true` |
|
|
2630
|
+
| `computeCloseTimeoutCeiling({slowestMs, overrideMs?})` | → `{ ceilingMs, source: 'estimated'\|'override' }` | `max(FLOOR, K × slowestMs)`, unless `overrideMs` is given, in which case it wins outright (may sit above OR below the estimate — not tighten-only) |
|
|
2631
|
+
| `resolveCloseTimeoutMs({job, stages, cwd, redact})` | → `{ closeTimeoutMs, source, timing, timedOut }` | the ONE resolver `src/planrun.js` and `scripts/run-u.mjs` BOTH call (never two independent spellings of this precedence): a signed `job.closeTimeoutMs` wins with NO timing pass run at all; absent, the pass runs and the ceiling autosets. `timedOut: true` means a stage never finished the pass — the caller reds `close-timing-red` rather than trusting any number |
|
|
2632
|
+
| `closeTimeoutBanner({ceilingMs, source, slowestMs?, slowestName?})` | → the printed line | ONE spelling for `estimated`/`override`/`explicit` (the last is the pre-M3 shell/test runtime-option escape hatch, never the signed field) — printed on every run so a silent default can never hide behind this feature the way the OLD 120s/900s defaults did |
|
|
2633
|
+
|
|
2634
|
+
**`scripts/sign-close.mjs`** — `node scripts/sign-close.mjs <jobs/x.json> [--write]` or
|
|
2635
|
+
`--all` for every `jobs/*.json`. Prints the old→new `jobSpecHash` and each stage's
|
|
2636
|
+
old→new sha256; NEVER writes unless `--write` is passed (a dry run is the default). The
|
|
2637
|
+
agent never runs this with `--write` on its own authority — minting a close-bytes
|
|
2638
|
+
signature is arbiter territory exactly like a budget or a wall clock, the same as the
|
|
2639
|
+
authoring pipeline's own refusal (`src/authorjob.js`'s `assembleSpec`, which refuses a
|
|
2640
|
+
draft carrying `sha256` the same way it refuses `close`/`closeDecl`/`verdictType`).
|
|
2641
|
+
|
|
2642
|
+
**`close[].sha256`** — a signed spec field (PRD item 27/M2): hex sha256 of the bytes of
|
|
2643
|
+
the script file the stage's `cmd` names (only for an addressable cmd, per the scope
|
|
2644
|
+
above). `validateJob` (`src/job.js`) demands it as `missing-required` on every addressable
|
|
2645
|
+
stage and shape-checks it (`invalid-value`, 64 lowercase hex chars) even where not
|
|
2646
|
+
demanded. It sits INSIDE the signed spec, so `jobSpecHash` covers it with no
|
|
2647
|
+
hash-function change — re-signing a job (minting or changing this field) moves the spec
|
|
2648
|
+
hash and needs re-approval, exactly like any other semantic edit.
|
|
2649
|
+
|
|
2650
|
+
#### `src/bundle.js` — pure, no provider/process calls
|
|
2651
|
+
|
|
2652
|
+
| function | args → returns | notes |
|
|
2653
|
+
|---|---|---|
|
|
2654
|
+
| `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. **F132:** a stage carrying a signed `close[].sha256` gets it RE-SIGNED in the written `spec.json` over the RELOCATED bytes (post import-rewrite) it actually packs — not copied verbatim from the source spec. Without this, a script whose import gets rewritten (`../src/x.js` → `'bareloop'`) ships with a signature that covers the wrong bytes, and the bundle refuses itself as `close-tampered` at $0 on its very first run (the manifest hash and the spec signature disagree over two different byte strings for the same file) |
|
|
2655
|
+
| `bundleHash(dir)` | `dir` → sha256 hex | recomputes from what is on disk right now; throws only if `dir` itself is not a directory |
|
|
2656
|
+
| `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 |
|
|
2657
|
+
| `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 |
|
|
2658
|
+
| `checkEnvelope(spec, envelope)` | `spec` (already resolved) + `{ budgetUsd?, maxWallMs? }` → `{ ok, reds }` | tighten-only: a given number must be ≤ the spec's own |
|
|
2659
|
+
| `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 |
|
|
2660
|
+
| `verifyBlessing(bundle)` | a `readBundle` result → `{ ok, unblessed, reds }` | `unblessed: true` (no red) means never blessed here; a red means blessed-but-stale |
|
|
2661
|
+
| `appendHistory(dir, row)` | `dir` + a plain object → `void` | atomic append to `history.jsonl` (`appendFileSync`, same convention as `spine.js`/`ledger.js`) |
|
|
2662
|
+
|
|
2663
|
+
**Every red code `src/bundle.js` can emit, and when:**
|
|
2664
|
+
|
|
2665
|
+
| code | fires in | meaning |
|
|
2666
|
+
|---|---|---|
|
|
2667
|
+
| `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 |
|
|
2668
|
+
| `outdir-not-empty` | `exportBundle` | `outDir` exists and already holds files — export never merges into a prior bundle |
|
|
2669
|
+
| `registry-unreadable` | `exportBundle` | `registryDir` itself could not be read (bubbled from `loadRegistry`) |
|
|
2670
|
+
| `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 |
|
|
2671
|
+
| `close-cmd-unrelocatable` | `exportBundle` | a `close[].cmd` is not exactly `node <absolute path>.mjs …` — the one shape this module can relocate under `$BARELOOP_BUNDLE` |
|
|
2672
|
+
| `close-script-missing` | `exportBundle` | a stage's `cmd` names a script `closeScripts` carries no source for |
|
|
2673
|
+
| `close-script-collision` | `exportBundle` | two different close script paths share one basename — a flat `close/` directory would silently clobber one |
|
|
2674
|
+
| `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 |
|
|
2675
|
+
| `close-import-unexported` | `exportBundle` | a close script imports a name from bareloop's own `src` that `src/index.js` does not export |
|
|
2676
|
+
| `close-absolute-path` | `exportBundle` (F129) AND `runPlan` (PRD item 27, every job) | a close script's source bakes in a quoted string literal that is an absolute POSIX path which `existsSync` finds real on this 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, `src/close-integrity.js`, for its named limits: string concatenation and cross-machine-only paths are invisible to it). `runPlan` reds this at run start, before the close-first precheck and before any provider call — $0, nothing spent |
|
|
2677
|
+
| `close-sha-mismatch` | `exportBundle` (PRD item 27/M2) | a `close[].sha256` the spec carries does not match the bytes of the script actually being packed into the bundle — the two signatures (this one, and `bundleHash`'s manifest hash over the rewritten script) cover the same bytes by two different paths and must never drift apart silently (N4's own hazard, one layer up). Only fires when a value is PRESENT and wrong; a spec with no sha256 at all reds `missing-required` upstream, at `validateJob` |
|
|
2678
|
+
| `close-tampered` | `runPlan` (PRD item 27/M2) | a close script's bytes no longer match its signed `sha256` — checked at run start (alongside `close-absolute-path`, before the close-first precheck and before any provider call) AND before EVERY close run thereafter (`runCloseStages`'s wrapper in `src/planrun.js`, the one seam the precheck/preflight/check-passes/close-fix-loop all share). Distinct from `close-red` (a judged "no" from a working close) and from `close-crashed`/`close-timeout`/`close-killed` (the close ran and hit an instrument fault): this is decided BEFORE the close is even spawned, and it means the close that ran would not have been the one the operator approved. Registered in `CLOSE_FAULTS` (`src/ralph.js`) so it rides out through the same forbidden-zone machinery as every other close fault — never retried, never fed back as a gap |
|
|
2679
|
+
| `close-dir-required` | `runPlan` (PRD item 27/M3 Part B) | a close script's source mentions `BARELOOP_CLOSE_DIR` (it reads its own books directory) but the runner was given no `closeDir` at all — `checkCloseDirRequired` (`src/close-integrity.js`) reuses the same `readCloseScripts` reader the other two integrity checks share. $0, run start, before the close-first precheck and before any provider call |
|
|
2680
|
+
| `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely) |
|
|
2681
|
+
| `wall-under-close-timeout` | `runPlan` (PRD item 27/M3) | `job.maxWallMs` is set and is under the EFFECTIVE per-stage close timeout just resolved (autoset or signed) — a budget under one close cannot fund its own close. `validateJob`'s own `MIN_WALL_MS` floor only bounds the un-autoset library default; this run-start check is the honest shape once the real ceiling is known, rather than a silent clamp |
|
|
2682
|
+
| `bundle-missing` | `readBundle` | `dir` is not a directory at all |
|
|
2683
|
+
| `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
|
|
2684
|
+
| `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** |
|
|
2685
|
+
| `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 |
|
|
2686
|
+
| `envelope-widen` | `checkEnvelope` | a given `budgetUsd`/`maxWallMs` exceeds the bundle's own signed ceiling — raising one is a re-export, never a runner flag |
|
|
2687
|
+
| `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 |
|
|
2688
|
+
|
|
2689
|
+
**N4 — why `readBundle`'s tamper check must run before anything else touches the bundle.**
|
|
2690
|
+
`jobSpecHash` covers `close[].cmd` as a *path*, not the script's bytes, so a swapped close
|
|
2691
|
+
script leaves the signed spec's own hash intact. `runJob`'s close-first precheck
|
|
2692
|
+
(`src/planrun.js`) would then read the swapped script's fake "already green" as a genuine
|
|
2693
|
+
one — a fake green, $0 spent, no work done, reported as success. `readBundle`'s
|
|
2694
|
+
`bundle-tampered` red is therefore not a courtesy check: it is the only thing standing
|
|
2695
|
+
between a swapped script and a fake green, so `bareloop run` calls it as literal step 1,
|
|
2696
|
+
before the envelope check, before the key check, before the worktree, before `runJob` is
|
|
2697
|
+
ever reached.
|
|
2698
|
+
|
|
2699
|
+
#### The CLI — `bareloop export | run | history`, `bin/bareloop.mjs`
|
|
2700
|
+
|
|
2701
|
+
`bin/bareloop.mjs` is a ~10-line adapter: it supplies the real `deps` (real `env`/`stdout`/
|
|
2702
|
+
`stderr`/`cwd`/`stdin`) and turns the returned number into `process.exitCode` — never
|
|
2703
|
+
`process.exit()`, which can discard queued stdout under a slow reader. All logic lives in
|
|
2704
|
+
`src/cli.js`'s `main(argv, deps)`, exported from the package root as **`cliMain`**
|
|
2705
|
+
(`export { main as cliMain } from './cli.js'` — renamed on export so it never collides with
|
|
2706
|
+
some other module's own generic `main`). `cliMain(argv, deps)` is the in-process test/
|
|
2707
|
+
integration seam: pass `deps.provider` (and optionally `providerFor`/`judgeProvider`) and the
|
|
2708
|
+
real `ANTHROPIC_API_KEY` check is skipped entirely — a scripted provider *is* the run, the
|
|
2709
|
+
same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
|
|
2710
|
+
`now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
|
|
2711
|
+
|
|
2712
|
+
Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
|
|
2713
|
+
quit` — and asks each sub-command's arguments one line at a time over the SAME code paths
|
|
2714
|
+
below.
|
|
2715
|
+
|
|
2716
|
+
- **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
|
|
2717
|
+
close-script paths against the spec file's own directory (never the process cwd), calls
|
|
2718
|
+
`exportBundle`. On success prints `bundleHash:`, the sorted file list, and "this bundle is
|
|
2719
|
+
unblessed until its first run greens on the importer's machine." Exit `1` on any red or
|
|
2720
|
+
missing flag, `0` on success.
|
|
2721
|
+
|
|
2722
|
+
- **`bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]`**
|
|
2723
|
+
— the exact order below; no step is ever reordered:
|
|
2724
|
+
1. `readBundle(bundleDir)` — the tamper check (N4, above). Any red stops here, exit `1`.
|
|
2725
|
+
2. `checkBundleDeps(bundleDir)` (F128) — can the bundle's own `close/` directory resolve
|
|
2726
|
+
`require.resolve('bareloop')`? A bundle installed the WRONG way (as someone else's
|
|
2727
|
+
dependency — see the corrected adopter flow below) has no `node_modules` of its own,
|
|
2728
|
+
which would otherwise crash the close deep inside the precheck with a bare
|
|
2729
|
+
`ERR_MODULE_NOT_FOUND`. A red here (`bundle-deps-missing`) prints the exact cure line
|
|
2730
|
+
(`cd <bundleDir> && npm install`) and stops, exit `1`, before the envelope check, the
|
|
2731
|
+
key, or any worktree.
|
|
2732
|
+
3. `checkEnvelope(spec, { budgetUsd?, maxWallMs? })` — `--wall` is minutes, converted to ms.
|
|
2733
|
+
A red (an invalid number, or a widen) stops here, exit `1`.
|
|
2734
|
+
4. The provider key: `deps.provider` if the caller injected one (the test seam), else
|
|
2735
|
+
`ANTHROPIC_API_KEY` from `env`. Absent and no injected provider: print the bundle's own
|
|
2736
|
+
`README.md` plus `bundleHash:`, spend nothing, **exit `0`** (this is a legitimate,
|
|
2737
|
+
non-error stop, not a red).
|
|
2738
|
+
5. Blessing. No `blessing.json` yet: print the "first run" notice + the minting-run line
|
|
2739
|
+
(F128-corrected — see below), the `bundleHash`, and **require** `--approve <bundleHash>`
|
|
2740
|
+
to match exactly, or exit `1`. A `blessing.json` present: `verifyBlessing` must pass
|
|
2741
|
+
(`blessing-stale` stops here, exit `1`); a `--approve` flag is accepted but ignored with
|
|
2742
|
+
a printed note ("no-resign" — hamr's ruling).
|
|
2743
|
+
6. The worktree: refuse (exit `1`) if `--repo` is not a git repo with ≥1 commit, or if
|
|
2744
|
+
`<repo>/.bareloop/wt/<runid>` already exists; otherwise `git worktree add --detach
|
|
2745
|
+
<repo>/.bareloop/wt/<runid> HEAD` — **always fresh, never reused** (a reused worktree
|
|
2746
|
+
would read a prior run's edits as "already-green" — a negative POC's exact finding).
|
|
2747
|
+
7. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/
|
|
2748
|
+
`maxWallMs` on that resolved spec in memory if `--budget`/`--wall` were given → mint a
|
|
2749
|
+
**fresh** `approveHash = jobSpecHash(that tightened+resolved spec)` (the runner signs
|
|
2750
|
+
the spec it is actually about to run; the human-approved `bundleHash` never changes) →
|
|
2751
|
+
`runJob(runSpec, { approvals: [{ specHash: approveHash, signer: 'bundle', ts }], workdir:
|
|
2752
|
+
worktree, provider, providerFor, judgeProvider, emit, shellCapUsd: runSpec.budgetUsd,
|
|
2753
|
+
readShim: 'cap', scout: true })`, spine written to `<bundleDir>/runs/<runid>/spine.jsonl`.
|
|
2754
|
+
A thrown `runJob` crash is caught and reported, exit `1`.
|
|
2755
|
+
8. The worktree's `gate-audit.jsonl` is moved to `<bundleDir>/runs/<runid>/gate-audit.jsonl`
|
|
2756
|
+
(same relocation `scripts/run-u.mjs` does for in-repo runs). `spentUsd`/`spendComplete`
|
|
2757
|
+
are read off this run's own `job-end` spine event — never fabricated as `0` when unknown.
|
|
2758
|
+
9. `appendHistory` — one `history.jsonl` line: `{ runid, at, outcome, spentUsd,
|
|
2759
|
+
spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash, approveHash }` (the
|
|
2760
|
+
`bundleHash` ↔ `approveHash` pairing, POC fact 2, so the human-signed hash and the
|
|
2761
|
+
hash actually enforced can never drift apart silently). A `green` outcome on a still-
|
|
2762
|
+
unblessed bundle also calls `bless(bundleDir, { bundleHash, runid, outcome, host })`.
|
|
2763
|
+
10. The tail: `outcome`, `spent` (`≥$…` when `spendComplete === false`, `UNKNOWN` when
|
|
2764
|
+
`spentUsd` is `null`), `branch`, `worktree`, a `git merge <branch>` instruction
|
|
2765
|
+
("merge stays human — this CLI never merges"), and "the worktree is kept until you
|
|
2766
|
+
remove it: `git worktree remove <worktree>`" (F110: never delete it for the user).
|
|
2767
|
+
**Exit `0` ONLY for `green`/`already-green` (F128) — every other outcome (close-red,
|
|
2768
|
+
plan-red, escalated, cap/wall-halted, provider-red, a crashed `runJob`, …) exits `1`,**
|
|
2769
|
+
so a caller scripting off the exit code can never mistake a red run for a green one.
|
|
2770
|
+
The tail's printed lines are unchanged either way.
|
|
2771
|
+
|
|
2772
|
+
**Adopter flow, corrected (F128, 2026-09-06 — hamr's paid fire found the frozen spec's
|
|
2773
|
+
validation step 2 named the wrong install shape):** `npm install <bundle dir>` run from a
|
|
2774
|
+
clean CONSUMER directory installs the bundle AS that consumer's own dependency — it never
|
|
2775
|
+
installs the bundle's OWN declared dependency (`package.json`'s `bareloop`), which its
|
|
2776
|
+
close scripts need at run time. The correct flow is **`cd <bundle> && npm install`** (reads
|
|
2777
|
+
the bundle's own `package.json`), then **`./node_modules/.bin/bareloop run . --repo <path>
|
|
2778
|
+
…`**. `checkBundleDeps` (above) is the fail-safe that catches the wrong-shape install
|
|
2779
|
+
before any close ever runs; see `docs/logs/FINDINGS.md` F128 for the full account
|
|
2780
|
+
(grounded in the real crashed spine) and `docs/product/EXPORT-BUILD.md`'s dated addendum.
|
|
2781
|
+
|
|
2782
|
+
**Minting-run line, corrected (F128).** The first-run notice used to print the shipped
|
|
2783
|
+
bridge's FIRST history row's `runid`, unconditionally. It now searches every bridge
|
|
2784
|
+
VERSION across all shipped bridges (base + shape fork) for the one whose `specHash` equals
|
|
2785
|
+
the resolved `$BARELOOP_BUNDLE` spec's `jobSpecHash`, printing `minting run: <runid> (spine
|
|
2786
|
+
not bundled in v1)` on a match or **`no version at this hash`** (never a guessed runid)
|
|
2787
|
+
when none matches — which, cross-machine, is the common case, since the historic mint's
|
|
2788
|
+
real close-script path and the importer's own bundle path essentially never coincide.
|
|
2789
|
+
|
|
2790
|
+
- **`bareloop history <bundleDir>`** → prints every `history.jsonl` line verbatim, then
|
|
2791
|
+
every bridge's `listingRow(b)` from `<bundleDir>/bridges`. Always exit `0`; "(no runs
|
|
2792
|
+
yet)" when `history.jsonl` doesn't exist.
|
|
2793
|
+
|
|
2794
|
+
**Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
|
|
2795
|
+
bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
|
|
2796
|
+
anything higher. Raising a ceiling is a re-export (a new signed bundle), never a runner flag.
|
|
2797
|
+
|
|
2798
|
+
**First-run approval, blessing, no-resign.** The bundle's own signed `bundleHash` is what a
|
|
2799
|
+
human reads and accepts with `--approve` on the run that first blesses it. That first GREEN
|
|
2800
|
+
writes `blessing.json`; every later run on that machine, against that same unchanged bundle,
|
|
2801
|
+
needs no `--approve` at all ("no-resign", hamr's ruling) — it only has to pass
|
|
2802
|
+
`verifyBlessing`. Editing the bundle after blessing (or re-exporting it) changes
|
|
2803
|
+
`manifest.bundleHash`, so the stored blessing goes stale (`blessing-stale`) and the human
|
|
2804
|
+
has to approve again.
|
|
2805
|
+
|
|
2806
|
+
**Fresh worktree per run; merge stays human.** Every `bareloop run` creates a brand-new
|
|
2807
|
+
`git worktree add --detach <repo>/.bareloop/wt/<runid>` off `HEAD` and hands that path to
|
|
2808
|
+
`runJob` as `workdir` — the caller's own checkout is never touched, and no worktree is ever
|
|
2809
|
+
reused (reuse would let a prior run's uncommitted edits masquerade as this run's own green).
|
|
2810
|
+
The CLI never merges anything itself: the tail prints the exact `git merge <branch>` command
|
|
2811
|
+
and leaves the worktree in place until the human removes it.
|
|
2812
|
+
|
|
2813
|
+
**`history.jsonl` row shape** — one JSON object per line, one line per `bareloop run` on this
|
|
2814
|
+
machine: `{ runid, at, outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree,
|
|
2815
|
+
branch, bundleHash, approveHash }`. `bundleHash` is the bundle's own signed manifest hash
|
|
2816
|
+
(unchanged across tightened runs); `approveHash` is the hash of the actual, possibly-
|
|
2817
|
+
tightened, `$BARELOOP_BUNDLE`-resolved spec that run executed — the two are recorded side
|
|
2818
|
+
by side so a tightened run's real approval can never be confused with the bundle's headline
|
|
2819
|
+
signature.
|
|
2820
|
+
|
|
2821
|
+
**Vocabulary:** `close-red` means the close **instrument** itself broke — it could not run,
|
|
2822
|
+
never finished, was killed, or crashed without judging anything (`CLOSE_FAULTS`, above) —
|
|
2823
|
+
never a judgment about the work. `plan-red` means the close ran, judged, and rendered a real
|
|
2824
|
+
**no**. A `readBundle`/`checkEnvelope`/`checkBundleDeps` red is a bundle-shape, envelope, or
|
|
2825
|
+
dependency refusal (before any provider call); it is none of those three — call it what it
|
|
2826
|
+
is: a bundle-red, an envelope-red, or a deps-red, not a close verdict. (F128's live defect
|
|
2827
|
+
would, unfixed, have shown up as `close-red` at runtime for a reason that had nothing to do
|
|
2828
|
+
with the close itself — `checkBundleDeps` exists precisely so a wrong-shape install never
|
|
2829
|
+
reaches that far.)
|
|
2830
|
+
|
|
2831
|
+
**What is NOT in v1 (hamr Q7):** the review door at the run's tail, `soft-green`, any
|
|
2832
|
+
provider but `anthropic-api`, spine bundling for `formatReplay` on the minting run (only its
|
|
2833
|
+
`runid` is noted), and tarball packing — a bundle is a plain directory; use `npm pack` on it
|
|
2834
|
+
yourself if you want a tarball.
|
|
2514
2835
|
|
|
2515
2836
|
## Architecture
|
|
2516
2837
|
|
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.22.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,13 +11,16 @@
|
|
|
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": {
|
|
17
20
|
"node": ">=20"
|
|
18
21
|
},
|
|
19
22
|
"scripts": {
|
|
20
|
-
"test": "node
|
|
23
|
+
"test": "node scripts/hermetic.mjs && node scripts/prompt-commit-check.mjs --range origin/main..HEAD",
|
|
21
24
|
"typecheck": "tsc --noEmit",
|
|
22
25
|
"build:types": "tsc",
|
|
23
26
|
"prepublishOnly": "npm run build:types"
|
|
@@ -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",
|
package/src/authorjob.js
CHANGED
|
@@ -654,7 +654,21 @@ export async function authorCloseForJob({
|
|
|
654
654
|
/** The fields `assembleSpec` writes, and therefore the fields a DRAFT must not
|
|
655
655
|
* carry. Named as data so the refusal and the fold can never disagree about
|
|
656
656
|
* which half of the spec is which. */
|
|
657
|
-
|
|
657
|
+
// `sha256` (PRD item 27/M2) rides alongside `close`/`closeDecl`/`verdictType`:
|
|
658
|
+
// it is arbiter territory exactly like a budget or a wall clock — the agent
|
|
659
|
+
// never mints a close-bytes signature (only `signCloseScripts`/hamr's own
|
|
660
|
+
// re-sign does), so a draft carrying the field at all is refused the same
|
|
661
|
+
// way one carrying a raw `close` array is. A `closeDecl` spec has no script
|
|
662
|
+
// path to sign in the first place (M1's own kind executor, never a shipped
|
|
663
|
+
// script), so this can only ever fire on a smuggled/legacy-shaped draft —
|
|
664
|
+
// named here anyway, because "arbiter fields are refused by name" is the
|
|
665
|
+
// rule, not "arbiter fields that are currently reachable are refused".
|
|
666
|
+
// `closeTimeoutMs` (PRD item 27/M3) joins the same list for the same reason:
|
|
667
|
+
// it is arbiter territory exactly like the timeout floor/multiplier
|
|
668
|
+
// themselves — the agent never mints a close-timeout override (only the
|
|
669
|
+
// operator's own signed spec edit does), so a draft carrying it is refused
|
|
670
|
+
// the same way one carrying a raw `close` array is.
|
|
671
|
+
export const AUTHORED_SPEC_FIELDS = Object.freeze(['close', 'closeDecl', 'verdictType', 'sha256', 'closeTimeoutMs']);
|
|
658
672
|
|
|
659
673
|
/**
|
|
660
674
|
* Fold an authored close into the OPERATOR's own half of the spec.
|