bareloop 0.33.0 → 0.35.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 +47 -0
- package/bareloop.context.md +263 -27
- package/package.json +1 -1
- package/src/bundlerun.js +13 -4
- package/src/draftspend.js +82 -0
- package/src/exits.js +10 -1
- package/src/interviewrun.js +10 -9
- package/src/job.js +39 -2
- package/src/judged.js +25 -5
- package/src/kinds.js +2 -0
- package/src/ledger.js +104 -9
- package/src/legs.js +171 -0
- package/src/monthly.js +133 -87
- package/src/panel/authorroutes.js +95 -15
- package/src/panel/authorsession.js +408 -113
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1071 -184
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +855 -81
- package/src/panel/status.js +51 -0
- package/src/planrun.js +63 -31
- package/src/providerrows.js +21 -2
- package/src/providers.js +4 -4
- package/src/ralph.js +3 -0
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +90 -7
- package/src/text.js +20 -0
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +166 -65
- package/types/draftspend.d.ts +53 -0
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +5 -2
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +39 -22
- package/types/panel/authorroutes.d.ts +38 -1
- package/types/panel/authorsession.d.ts +109 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +255 -2
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +14 -1
- package/types/providers.d.ts +1 -2
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +66 -2
- package/types/text.d.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ 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.35.0] - 2026-10-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **One run = one id (resume).** A resume now continues the SAME run: one runid, one spine file, a `leg-resume` marker opens each new leg (`legsOf` is the one owner of leg boundaries). Outcome is the last leg's, money is each leg's own spend summed once, wall is the sum of leg windows with the gap between legs excluded. The run list keeps one row; the panel shows one card (`resumed xN`), an Audit leg divider, and a dotted map connector. Resume readers (`readResume`, pause age gate, `deathAtOf`) read the latest leg.
|
|
13
|
+
- **Panel Stop.** `POST /api/runs/:runid/stop` writes a stop request that is read at the round boundary where the money cap binds (not between steps), so a one-step run can be stopped. The leg ends as the resumable outcome `stopped` (`stopped` joins the checkpoint outcomes); resume re-enters the cut step. The page says "stopping after this turn...".
|
|
14
|
+
- **Panel Resume.** `POST /api/runs/:runid/resume`, a Resume mode on the run's Job tab where only the money and time caps are inputs ("Money cap ($)", "Time cap (min)", Sign & resume). Changed caps write `resolved-spec-r<k>.json` beside the original. A cap at or below what is already spent or used is refused at $0.
|
|
15
|
+
- **Ended block.** Every finished run carries one code-owned reason / next line (`ended` on the run detail, `endedLine` on the run list), shown on the Audit tab and under the job name on the card. Resume is listed only when the engine would accept it; a strike-governor cap-halt is no longer read as the money cap.
|
|
16
|
+
- **Reuse workflow.** Green runs (and imported jobs) offer [Reuse workflow] in the Run tab's top action row: the origin's signed spec is copied and only five boxes are open (Source, Destination, money cap, time cap, Model; the server refuses any other change). `workflowKey(spec)` identifies "the same job" (signed spec without source, destination, budget, wall, provider/model; `jobSpecHash` untouched). The estimate line reads "Same job - G green - N not green - about $X and M min a run", with unknown said, never rendered as $0. Red rows say "Change the job: + New."
|
|
17
|
+
- **Read-only Import.** A guarded folder browser (`GET /api/fs/list`, folders only, no symlinks, depth-3 search for job bundles, remembers the last folder), `POST /api/imports`, `GET /api/imports[/:id]`, rows in `<home>/imports.jsonl`, re-read on every view ("changed since import", "folder not found"). An imported job opens in the normal Run / Audit / Job tabs: an IMPORTED box, then the SUMMARY and MAP of its latest green run (from the bundle's spine, else its bridge, else "no green run in this bundle"). Reuse re-verifies the bundle and every close stage's signed bytes at the moment of reuse; the person signs it here. Re-importing a folder updates its one row.
|
|
18
|
+
- **Runs list.** Runs/workflows first, imported jobs below; one code-owned status table (sign to word) read by run cards, expanded rows, the header, imported rows and MAP step cards; step names as written; Audit step rows wear the Run tab's sign and word.
|
|
19
|
+
- **Monthly claims.** Settled per leg: a resumed run stays one listed row and only the leg's claim is released.
|
|
20
|
+
- **Chat panel rework.** One chronological progress list (every pipeline step is one line that updates in place; a resumed step appends a new line; detail on its own line; one drafting line carrying model and cap), sitting under the thread above the ask box and auto-scrolling when it changes while live. One wide main button whose wording follows the moment (Start drafting / Sign & run / Check again / Send / Sign & run). The Chat tab always opens on an empty job card with Clear (or Abandon while a session is live and unsigned) on the card; `+ New` is gone. Ask box is a wrapping textarea (Enter sends, Shift+Enter is a newline) dimmed until there is something to reply to; job-card free-text boxes wrap and auto-grow; focused field gets the accent border. The card is fully locked while a drafting session is live.
|
|
21
|
+
- **`POST /api/author/:id/abandon`** ends a live authoring session: terminal phase `abandoned`, releases the one-at-a-time lock, keeps booked drafting spend, books an in-flight call's usage, starts no new model call.
|
|
22
|
+
- **`GET /api/author/live`**: a refreshed Chat page re-attaches to the one live authoring session (reuse included), card refilled from the session's own copy.
|
|
23
|
+
- **Run summary** counts steps, not parts (a stopped-then-continued step is one step); a halted step's time stops at its leg's end.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **F122 2A (b):** a step worker round cut at `max_tokens` (`truncated:max_tokens`) is now a failed attempt on the strike ladder, not provider-red; the same holds for the close-fix loop (the cut round is discarded, its spend stays booked, the next fix prompt carries a cut-off note). Scout, drafter and replan keep provider-red.
|
|
28
|
+
- **F212:** a single-file `writeScope` is now seen by the tree snapshot, so `tree-changed` no longer reads 0 files changed when the worker edited that file.
|
|
29
|
+
- **F213:** drafting spend of an authoring session that never became a run is booked (`draft-spend.json` written after every metered call, counted by the monthly and Money readers, attributed by start time).
|
|
30
|
+
- Authoring refusals at $0: a repo source's absolute Destination is refused before any model call (one rule for the fresh and Reuse card; a repo Destination is the write fence); the non-repo refusal names what the door found (single file / plain folder / other); the reuse refusal names the provider in plain words.
|
|
31
|
+
- A dead panel token (server restarted) shows one reload line and stops the Chat poll; a session's refusal is recorded once, in the progress line only.
|
|
32
|
+
- Panel: a live run headline no longer reads `undefined` for wall time; a run with no spine yet reads "starting..."; live cards carry the same money/time floors as the right pane; Ended reasons are plain words with no raw engine detail; the IMPORTED box names the run behind each exported-history price; Providers keys-file path is never a hardcoded guess.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Tests that spawn a real runner take HOME and keys from a scratch dir (`cleanEnv()`), and pause/checkpoint fixtures are relative to today (the 60-day pause TTL expired a literal 2026-08-04 date). Test-only.
|
|
37
|
+
- Small refactors: one spelling of the panel bind address in the Host and Origin guards; judge-round `rateSource` uses the one `rateSourceFields` helper; `monthly.js` header describes the held-money refusal form.
|
|
38
|
+
|
|
39
|
+
## [0.34.0] - 2026-10-01
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Judge rounds now carry `rateSource` (`'caller'` under your price, else the guess label the worker rounds carry), so a customer-priced judge round counts as vouched, not other, in the provenance readout and the monthly counts. Dollar totals are unchanged. Evidence: live run `mup3h70u` (136 worker rounds `caller`, 14 judge rounds without it).
|
|
44
|
+
- The monthly-limit refusal now names the money a run in progress holds: `Max $4.00 (monthly limit, $5.95 held by a run in progress)` (`runs` when several); with nothing held the text is unchanged. `monthSpend` / `checkMonthlyRoom` also return `heldUsd` and `heldRuns`.
|
|
45
|
+
- The interview's "Run it now?" now starts the packaged door (`bin/bareloop.mjs author`, same flags) instead of `scripts/run-author.mjs`, which the published package does not ship.
|
|
46
|
+
- One "is this run alive" rule (`runIsAlive`, `src/runlist.js`) now drives both the panel's [▶] / [?] glyph and the monthly limit's hold: the run row's pid first (`isLiveRunner`), the spine-mtime rule only for a row with no pid. A run whose pid is alive but whose spine is quiet for 10+ minutes now reads running, not died; a dead pid with a fresh spine reads died. A `job-end` on the spine still wins.
|
|
47
|
+
- `--resume` now also refuses when the predecessor run's own run-list row carries the pid of a live bareloop runner, not only when a watchdog kill record names it (same refusal text).
|
|
48
|
+
- Every panel GET/HEAD route now refuses (`403`) a request whose `Host` is not the panel's own `127.0.0.1:<port>` (one shared `checkHostGuard`, also used by the write routes' guard), so a page on another origin cannot read the run list through a rebound DNS name. No token is needed to read.
|
|
49
|
+
- The monthly-limit check (and so every run start) no longer parses the spine of every run ever listed: it reads only runs listed in the current local month, plus any claim still held by a live runner. The Money tab's all-time figures still read everything.
|
|
50
|
+
- A key row priced 0 / 0 is still accepted, but the `price yours:` readout (run preview and run tail) now ends with a warning that the price is $0 so spend and limits read $0.
|
|
51
|
+
- A run that stops at $0 on a SOURCE-MANIFEST-RED, DESTINATION-RED or CLOSE-TIMING-RED now releases its monthly claim (the same `released` entry as a run that never started), so it leaves no ghost row and the month no longer reads "at least".
|
|
52
|
+
- A spec whose `budgetUsd` is not a finite number (e.g. `1e999`) is refused by `validateJob` at $0, and the monthly-limit check (`checkMonthlyRoom` / `claimRun`) no longer reads a non-finite cap as $0 — it never fits.
|
|
53
|
+
- A bundle money halt that used the whole signed ceiling now says no room is left under the signed ceiling (and prints no resume line) instead of a `--budget <more than X, at most X>` placeholder nothing can satisfy.
|
|
54
|
+
|
|
8
55
|
## [0.33.0] — 2026-09-30
|
|
9
56
|
|
|
10
57
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -158,7 +158,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
158
158
|
| `provider` | `anthropic-api` \| `openai-api` \| `gemini-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-flash` today (F171: swapped from the retired `deepseek-chat`) — see "Worker surface" above for its `baseUrl` field and per-model gating; `gemini-api` (PRD item 31.3) reads `GEMINI_API_KEY` and is ADMITTED-PENDING-PROBE — zero runs, and every launch says so (`PROBE_STATUS`/`probeWarningLines`); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
159
159
|
| `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) |
|
|
160
160
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
161
|
-
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
161
|
+
| `budgetUsd` | finite `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
162
162
|
| `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 |
|
|
163
163
|
| `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 |
|
|
164
164
|
| `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's model comes from the `judge` row below, which DEFAULTS to this same worker model |
|
|
@@ -1112,7 +1112,9 @@ from ralph's shipped `runClose` because one runs a DECLARATION and the other an
|
|
|
1112
1112
|
|
|
1113
1113
|
The shell's own fixed code for the closed exit menu — nothing here executes
|
|
1114
1114
|
agent-authored text. `snapshotScope` hashes every file under a scope prefix (the
|
|
1115
|
-
"before" side of `tree-changed`; a missing dir snapshots empty
|
|
1115
|
+
"before" side of `tree-changed`; a missing dir snapshots empty; a scope whose prefix names a single
|
|
1116
|
+
regular FILE — a file-valued `writeScope` becomes `src/x.js/**` — snapshots that one file, keyed by the
|
|
1117
|
+
prefix, F212: a readdir on a file used to read as an empty tree forever). `evalExits` is AND-only
|
|
1116
1118
|
and never short-circuits — the result names EVERY failing wall (`{ pass, results }`,
|
|
1117
1119
|
each result `{ type, pass, detail?, fault? }`). `tree-changed` reads OUTCOME (bytes vs
|
|
1118
1120
|
the snapshot): an identical re-write is NOT a change (F43) and git status is never
|
|
@@ -1229,7 +1231,7 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
|
|
|
1229
1231
|
|
|
1230
1232
|
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
1231
1233
|
plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
|
|
1232
|
-
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled |
|
|
1234
|
+
provider-red | interpreter-red | cap-halt | wall-halt | stopped | step-stalled |
|
|
1233
1235
|
step-red:<id> | runner-drained`.
|
|
1234
1236
|
|
|
1235
1237
|
**`runner-drained` (F140/PRD item 28(c))** is a distinct class from every outcome above: it is
|
|
@@ -1380,9 +1382,13 @@ ambiguity, not a merge. `resolveHumanRuling(fresh, held)` is the exported seam t
|
|
|
1380
1382
|
`branch-red` is the WORK BRANCH refusing (below): the patient is not a git checkout, its
|
|
1381
1383
|
branch namespace has no free name, or a resume's recorded branch is gone. Zero tokens, and
|
|
1382
1384
|
never a fallback to working on the branch the run was handed. `provider-red` is a
|
|
1383
|
-
transport throw or a
|
|
1384
|
-
BA-6 — before which it laundered into a clean finish, F25): no
|
|
1385
|
-
round's spend is only partly known (F6).
|
|
1385
|
+
transport throw, or a scout/drafter/replan round the API cut off mid-generation
|
|
1386
|
+
(`truncated:max_tokens`, BA-6 — before which it laundered into a clean finish, F25): no
|
|
1387
|
+
verdict exists and the failed round's spend is only partly known (F6). A STEP or FIX-loop
|
|
1388
|
+
worker round cut at `max_tokens` is NOT provider-red (F122 "2A" half b, hamr 2026-10-05, fix
|
|
1389
|
+
phase added by ruling A): it is a failed attempt — the round is booked, its output discarded,
|
|
1390
|
+
an `attempt-bounded` record carries the reason `output cut at the token limit (max_tokens)`,
|
|
1391
|
+
and the step's strike ladder (or the fix governor) applies as for any failed attempt. `cap-halt` is the wallet; `wall-halt` is the clock
|
|
1386
1392
|
(F64 — a timeout derived from the run's own deadline is a governance stop, never a transport
|
|
1387
1393
|
casualty). **`cap-halt` reaches you from the close-fix loop too, not only from a step:** the
|
|
1388
1394
|
shell spells exhaustion (strikes on a step, and — since v1.46 — strikes in the fix loop as
|
|
@@ -1860,10 +1866,10 @@ before. bareloop keeps no price list of its own, ever.
|
|
|
1860
1866
|
carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
|
|
1861
1867
|
`src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
|
|
1862
1868
|
`pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
|
|
1863
|
-
(the guess).
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1869
|
+
(the guess). `judge-round` records carry it too (the judge Loop's per-call label, forwarded by
|
|
1870
|
+
`src/kinds.js` `onJudgeCost`): `'caller'` under your price on the judge's row, else the guess label. The
|
|
1871
|
+
exceptions read UNKNOWN provenance, correctly, and by the same rule that governs the archive: every
|
|
1872
|
+
round archived before the signal existed (judge rounds before this change included). The native
|
|
1867
1873
|
per-turn `worker-turn` carries `null`, bareloop's OWN statement rather than a forwarded one: that
|
|
1868
1874
|
surface prices the SESSION, so a turn had no rate to guess. A `worker-round`/`worker-turn`
|
|
1869
1875
|
carries bare-agent's own label beside `pricing`:
|
|
@@ -2320,6 +2326,47 @@ watchdog `--wall-ms 0`, which it defaulted to `null` and armed no deadline at al
|
|
|
2320
2326
|
still advertising one. The refusal names the lever: raise `maxWallMs`, which moves the spec hash
|
|
2321
2327
|
and is re-signed.
|
|
2322
2328
|
|
|
2329
|
+
**A resume is the SAME run — one runid, one spine file, a `leg-resume` marker inside (P5-R, hamr
|
|
2330
|
+
2026-10-02: "one run, one id, one file").** `bareloop run-u --resume <runid|path>` and `bareloop run
|
|
2331
|
+
<bundle> --resume <runid>` never mint a new id and never open a second file: the new leg APPENDS to the
|
|
2332
|
+
halted run's own spine (`u-<runid>.jsonl`, or `<bundleDir>/runs/<runid>/spine.jsonl`).
|
|
2333
|
+
- **The leg's first record is `leg-resume {leg: N, after, at}`** — `after` is how the previous leg ended
|
|
2334
|
+
(its `job-end` outcome, `'died'` when it recorded none) — written BEFORE the outside watchdog spawns (the
|
|
2335
|
+
watchdog judges a run dead by its file going quiet). `seq` continues from the file's own highest
|
|
2336
|
+
(`makeSpine(file, {startSeq})`). Then the leg's own `job-start`, which keeps the declared fold
|
|
2337
|
+
(`priorSpentUsd`/`priorWallMs`/…): the budget ceiling still folds prior spend.
|
|
2338
|
+
- **A torn tail is never edited.** If the file does not end in `\n` (a kill mid-append), the resume writes
|
|
2339
|
+
ONE `\n` and starts a fresh line: the torn bytes stay exactly as they were, alone on their own line
|
|
2340
|
+
directly before the marker. The spine is append-only forever.
|
|
2341
|
+
- **`legsOf(events)` (`src/legs.js`, internal — not exported from the package root) is the ONE owner of where
|
|
2342
|
+
a leg starts and ends**, returning `{leg, start, end, records, jobStart, jobEnd, outcome, after, startMs,
|
|
2343
|
+
endMs}`; a spine with no marker (every run written before P5-R, and every run nobody resumed) is ONE leg.
|
|
2344
|
+
`parseSpineText` tolerates exactly one torn line directly before a marker (and the file's own last two
|
|
2345
|
+
lines); any other unparseable line is a corrupt log. Every reader goes through it: the resume reader
|
|
2346
|
+
(`readResume` windows a direct spine at the latest leg that reached a `job-start`; `checkpointAgeGate` and
|
|
2347
|
+
`deathAtOf` look only at the latest leg's terminal), `replayRun`, the monthly limit, the run list, the
|
|
2348
|
+
panel, the end-of-run readout (this leg only), `bundlerun`'s history.
|
|
2349
|
+
- **Money has ONE basis per reader: the sum of each leg's own rounds** (`SPEND_RECORD_TYPES`, echoes
|
|
2350
|
+
excluded; `legSpend`/`runSpend`/`chainSpend` in `src/ledger.js`) — never the rounds plus a declared prior,
|
|
2351
|
+
so leg 1 is counted once. **Time is the sum of each leg's own window, the gap between legs excluded**
|
|
2352
|
+
(only working time charges the wall). **The outcome is the LAST leg's.** A run is live iff its last leg has
|
|
2353
|
+
no `job-end` and its runner is alive. An unknown figure reads as unknown / "at least", never as less.
|
|
2354
|
+
- **Side files are per leg.** The gate audit: leg 2+ APPENDS to `u-<runid>-gate-audit.jsonl` (never a
|
|
2355
|
+
rename-overwrite). The watchdog's kill note: `<spine>.leg<N>.watchdog.json` for N ≥ 2 (`u-watchdog.mjs
|
|
2356
|
+
--report <path>`; leg 1 keeps `<spine>.watchdog.json`). `.lag.jsonl` accumulates.
|
|
2357
|
+
- **The run list keeps ONE row per run.** A resume appends `{type:'leg-start', runid, leg, pid, capUsd, at}`;
|
|
2358
|
+
`readRunList` folds it: the row's live `pid`/`capUsd` are the latest leg's, and it carries `leg`. `settled`
|
|
2359
|
+
and `released` entries carry an optional `leg` (absent = leg 1): a later leg's `released` gives back THAT
|
|
2360
|
+
leg's claim and never removes the run, and the monthly hold is taken per leg (`claimRun({leg})`; a
|
|
2361
|
+
resumed leg's own earlier legs count as spent money, never as a hold).
|
|
2362
|
+
- **Refusals stay**: a run whose last leg is live (pid from the latest `leg-start`), a green run, or a
|
|
2363
|
+
non-resumable terminal is refused. `bareloop run`'s `history.jsonl` writes one row per LEG
|
|
2364
|
+
(`leg: N` from leg 2, no `resumedFrom`) and `run.json` is rewritten unchanged (same worktree, the run's
|
|
2365
|
+
own start).
|
|
2366
|
+
- **Not covered by this ruling, and unchanged:** the review-door `rerun` (`--door <runid> --decide rerun`)
|
|
2367
|
+
is a NEW run with a new runid (its door reader folds the answered run's chain spend); Reuse workflow in
|
|
2368
|
+
the panel is a new run.
|
|
2369
|
+
|
|
2323
2370
|
**A pause checkpoint is answered on the same command line** (N4, 2026-08-12 §5.2 — the
|
|
2324
2371
|
terminal is the v1 surface; the panel is N6's). The reference runner takes
|
|
2325
2372
|
`--decide accept|rerun|pause` with `--text` for the rerun door, gated on the SAME
|
|
@@ -2997,8 +3044,8 @@ separate tarball step).
|
|
|
2997
3044
|
README.md the operator questions, in the order `bareloop run` asks them
|
|
2998
3045
|
blessing.json absent at export; written by the run that first greens it
|
|
2999
3046
|
history.jsonl absent at export; one line appended per `bareloop run` on this machine
|
|
3000
|
-
runs/<runid>/ absent at export; one per
|
|
3001
|
-
close's books) and run.json ({runid, worktree, seed, repo, at
|
|
3047
|
+
runs/<runid>/ absent at export; one per RUN (a resume continues it) — spine.jsonl, gate-audit.jsonl,
|
|
3048
|
+
close/ (the close's books) and run.json ({runid, worktree, seed, repo, at})
|
|
3002
3049
|
```
|
|
3003
3050
|
|
|
3004
3051
|
**`bundleHash`** = sha256 over the sorted `path:contentSha256` lines of exactly `spec.json`
|
|
@@ -3165,6 +3212,9 @@ monthly limit, your price and the key-row choice apply exactly as on the sub-com
|
|
|
3165
3212
|
got none, and a run started from the menu skipped the limit). All three are dispatched by name only: `bareloop run-u <flags…>`,
|
|
3166
3213
|
`bareloop interview <flags…>`, `bareloop author <flags…>`.
|
|
3167
3214
|
|
|
3215
|
+
When two key rows share one provider and endpoint, a CLI run uses the FIRST row in the keys list, so put the
|
|
3216
|
+
key you want first (the panel picks the row by the Model menu).
|
|
3217
|
+
|
|
3168
3218
|
- **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
|
|
3169
3219
|
close-script paths against the spec file's own directory (never the process cwd), calls
|
|
3170
3220
|
`exportBundle`. On success prints `bundleHash:`, the sorted file list, and "this bundle is
|
|
@@ -3201,11 +3251,13 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3201
3251
|
different from the recorded one is a stop, exit `1`, spending nothing; `--repo` is optional
|
|
3202
3252
|
on a resume) and hands the engine `resumeRun` on that run's spine — the engine's own gates
|
|
3203
3253
|
(checkpoint age, tree-at-seed, liveness) and spend fold apply, so `--budget` can never widen
|
|
3204
|
-
a resume. The new leg
|
|
3254
|
+
a resume. The new leg appends to the SAME `runs/<runid>/spine.jsonl` behind a `leg-resume` marker
|
|
3255
|
+
(P5-R): no new id, run dir or worktree. The resume command a halted leg prints carries that leg's own
|
|
3205
3256
|
`--budget`/`--wall`, only if you passed them (a tightened ceiling never silently reverts to the signed
|
|
3206
3257
|
one). After a MONEY halt the line does NOT repeat the exhausted `--budget` (chain spend is folded, so
|
|
3207
3258
|
it would halt again at once); it prints `--budget <more than $<spent so far>, at most $<signed
|
|
3208
|
-
budgetUsd>>` for you to fill in, and is not runnable until you do.
|
|
3259
|
+
budgetUsd>>` for you to fill in, and is not runnable until you do. When the run spent the WHOLE signed
|
|
3260
|
+
ceiling, that range is empty, so no resume line is printed: the block says no room is left under the signed ceiling. A bundle's spec is
|
|
3209
3261
|
manifest-hashed, so its halt hint never asks you to edit `budgetUsd`.
|
|
3210
3262
|
6. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/`maxWallMs`
|
|
3211
3263
|
in memory if `--budget`/`--wall` were given → `approveHash = jobSpecHash(that tightened+
|
|
@@ -3219,7 +3271,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3219
3271
|
inputs); `--door`/`--decide`/`--review-door` are not exposed on `run`.
|
|
3220
3272
|
8. `appendHistory` — one `history.jsonl` line off this leg's own `job-end`: `{ runid, at,
|
|
3221
3273
|
outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash,
|
|
3222
|
-
approveHash,
|
|
3274
|
+
approveHash, leg? }` (the `bundleHash` ↔ `approveHash` pairing, POC fact 2; one row per LEG of the
|
|
3275
|
+
one run — `leg: N` from the second leg, no `resumedFrom`).
|
|
3223
3276
|
`spentUsd`/`spendComplete` are never fabricated as `0` when unknown. No `job-end` (a $0
|
|
3224
3277
|
refusal, a crash) writes no row. A `green` outcome on a still-unblessed bundle also calls
|
|
3225
3278
|
`bless(bundleDir, { bundleHash, runid, outcome, host })`.
|
|
@@ -3291,7 +3344,7 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3291
3344
|
$0: no interview, no author, no run trigger, no key ever read. A row is `{ at, runid, job,
|
|
3292
3345
|
spine, patient, via }` — `spine`/`patient` are always absolute paths (`patient: null` when
|
|
3293
3346
|
not known, e.g. every `via:"backfill"` row: no spine record carries a run's workdir);
|
|
3294
|
-
`via` is `'run-u'`, `'bundle'`, or `'backfill'`. `bareloop run-u` and `bareloop run` (the
|
|
3347
|
+
`via` is `'run-u'`, `'bundle'`, or `'backfill'`. A RESUMED run is still one row: `readRunList` folds its `leg-start` entries into it (`leg`, and the latest leg's `pid`/`capUsd`; P5-R). `bareloop run-u` and `bareloop run` (the
|
|
3295
3348
|
bundle path) each append their own row at run START, before the first paid call — a
|
|
3296
3349
|
list-append failure is caught at both call sites and printed loudly to stderr
|
|
3297
3350
|
(`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (…)`); the run itself
|
|
@@ -3325,6 +3378,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3325
3378
|
`baselineKind`/`question`: the plain-English question is derived in `src/panel/server.js`
|
|
3326
3379
|
(`stageQuestionText`) from the signed stage's kind/params only, `null` when no honest
|
|
3327
3380
|
wording exists (the client then shows the stage's own name). Every
|
|
3381
|
+
GET/HEAD answers only when its `Host` header is the panel's own `127.0.0.1:<port>` (`checkHostGuard`,
|
|
3382
|
+
`src/panel/authorroutes.js`; any other Host is `403`, no token needed to read). Every
|
|
3328
3383
|
P1 endpoint is GET/HEAD only (anything else — including every write verb — is `405`, outside the
|
|
3329
3384
|
two human-click-guarded write families `/api/author/*` and `/api/settings/*` below); a URL
|
|
3330
3385
|
never joins a path segment into a filesystem read — a runid is looked up in the run list
|
|
@@ -3370,7 +3425,8 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3370
3425
|
requires the human-click guard: an `x-bareloop-token` header matching a fresh token minted
|
|
3371
3426
|
once per server start (templated into `index.html` like the port) AND an Origin/Host
|
|
3372
3427
|
naming this exact `127.0.0.1:<port>` — a request failing either gets `403`, before the
|
|
3373
|
-
route body ever runs.
|
|
3428
|
+
route body ever runs. The token protects against other browser origins, not against other local users or
|
|
3429
|
+
processes on the same machine (local-trust model). `POST /api/author/start` (one job-card body; `409` while a prior
|
|
3374
3430
|
session is still non-terminal — one authoring session at a time) creates a session
|
|
3375
3431
|
(`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
|
|
3376
3432
|
`authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
|
|
@@ -3378,9 +3434,17 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3378
3434
|
never a reimplementation of any gate. `GET /api/author/model-check?model=<id>` is the $0 readiness probe the job card runs
|
|
3379
3435
|
before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
|
|
3380
3436
|
completion; the page sees the key's NAME and a status word, never the value). `POST
|
|
3381
|
-
/api/author/:id/check-deps` is the install-gap's "Check again" (`phase:'install-needed'`:
|
|
3437
|
+
/api/author/:id/check-deps` is the install-gap's "Check again" (the main button's wording while waiting on an install) (`phase:'install-needed'`:
|
|
3382
3438
|
the session waits on the person's own install, then re-runs `missingDependencies` on the
|
|
3383
|
-
same copy — bareloop never installs). `
|
|
3439
|
+
same copy — bareloop never installs). `POST /api/author/:id/abandon` (the Chat card's **Abandon** button, 2026-10-05) ends a
|
|
3440
|
+
LIVE, unsigned session: phase becomes the terminal `abandoned` (releasing the one-at-a-time lock), money already booked
|
|
3441
|
+
stays booked (`draftSpentUsd` is never zeroed; a call in flight still books its usage), no NEW model call starts;
|
|
3442
|
+
`400` on a session that is not live, `404` unknown id, same guard as the other author routes. After `signed` the card's
|
|
3443
|
+
button reads **Clear** and only empties the card (a run is stopped from the Run tab). `GET /api/author/live` →
|
|
3444
|
+
`{ok, sessionId, state}` of the ONE live (non-terminal) session, or `{sessionId:null, state:null}`: a refreshed page
|
|
3445
|
+
re-attaches to it from the server's session map (nothing is stored page-side). A panel restart mints a new token and
|
|
3446
|
+
loses the in-memory sessions: the page then shows one "reload this page" line and stops the Chat poll (no auto-reload;
|
|
3447
|
+
drafting money already spent stays booked via `draft-spend.json`, below). `GET /api/author/:id` polls the session's state
|
|
3384
3448
|
(phase, chat messages, cost, `revisesLeft`, `specHash` once prepared). `POST /api/author/
|
|
3385
3449
|
:id/send {text}` answers whatever the confirm turn is currently asking (the `language` pick or a plan's
|
|
3386
3450
|
own follow-up question — the old `worseThanBefore` ask is retired, 2026-09-28) — refused outright when the pending ask is the
|
|
@@ -3405,6 +3469,167 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3405
3469
|
`signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
|
|
3406
3470
|
`~/.config/bareloop/.env` keys-file loader, and Settings have landed (P4a/P4b — see "Settings" below).
|
|
3407
3471
|
|
|
3472
|
+
- **Panel P5 — the Ended block (`docs/product/PANEL-BUILD.md` Addendum 2026-10-02, item 1)** →
|
|
3473
|
+
`GET /api/runs/:runid` carries `ended` = `{reason, next, line, actions:[{id,label}]}` for every
|
|
3474
|
+
FINISHED run (green too; `null` while the run is live), and `GET /api/runs` carries `endedLine`
|
|
3475
|
+
(the card's one short line under the job name, e.g. `money cap — resume`). Both come from ONE
|
|
3476
|
+
code-owned function, `endedFor` (`src/panel/server.js`): fixed sentences keyed by the run's
|
|
3477
|
+
outcome, with only
|
|
3478
|
+
numbers and the engine's own recorded detail slotted in — never model text. The table:
|
|
3479
|
+
green/already-green → "Goal met." (a `destination-refused` record turns it into "…the output
|
|
3480
|
+
could not be delivered (detail)."); cap-halt → "Money cap reached ($X of $Y)." / "Raise the cap,
|
|
3481
|
+
then Resume."; wall-halt → "Time cap reached."; provider-red → "The model provider failed
|
|
3482
|
+
(detail)."; step-stalled → "A step stopped making progress."; plan-red/check-red/step-red/escalated
|
|
3483
|
+
→ "Goal not met — the checks said no (last gap)."; close-red → "The check itself broke
|
|
3484
|
+
(instrument fault), not your goal."; `escalated` is a terminal and never resumable — its escalation of
|
|
3485
|
+
category `cap-halt` with `spend.strikes` is the STRIKE governor, read "The fix loop stopped improving
|
|
3486
|
+
(S of L tries, no check got better)." (card line `stopped improving`), and is the money cap ONLY when
|
|
3487
|
+
the spine has a `money-halt` record or the job-end outcome is `cap-halt` itself; every other terminal → "Stopped before or outside the work
|
|
3488
|
+
(code: <outcome>)." (never the raw engine detail — it can name retired surfaces; raw detail appears only as the quoted check gap on "goal not met" and the provider error, both capped at 120 characters; a destination-refused green reads "…could not be delivered." with no detail); a run with no `job-end` whose process is gone (glyph `?`, never `✗`) →
|
|
3489
|
+
"Stopped with no ending recorded (last thing it did: …)." `actions` holds `resume` ONLY when
|
|
3490
|
+
the engine would accept a resume (`resumePlanFor`: the same `readResume`/`CHECKPOINT_OUTCOMES`/
|
|
3491
|
+
`checkpointAgeGate` readers the engine refuses with, plus a signed `resolved-spec.json` /
|
|
3492
|
+
`resolved-spec-r<k>.json` beside the run whose hash is the one the run's `job-start` carries);
|
|
3493
|
+
otherwise `next` says why Resume is not available. `resume` on the detail is
|
|
3494
|
+
`{budgetUsd, maxWallMin, spentUsd, spendComplete}` (the confirm box's prefill) or `null`. The
|
|
3495
|
+
result glyphs are unchanged. The reuse button is described under Reuse workflow, below.
|
|
3496
|
+
**Status words (click-through 2026-10-04)** → every run row, run detail and imported row carries `status`
|
|
3497
|
+
`{key, sign, word}` from ONE table (`src/panel/status.js`): `[▶] running · [·] waiting · [✓] passed · [✗] failed ·
|
|
3498
|
+
[✗] capped · [✗] stopped · [?] died`; `glyph` is that sign. Mapping: green/already-green/satisfied → passed; no
|
|
3499
|
+
job-end + runner alive → running, gone → died; cap-halt, wall-halt, and an escalation that is a money-halt or wall-halt
|
|
3500
|
+
→ capped; `stopped` → stopped; every other outcome (plan-red, check-red, step-red, close-red, provider-red,
|
|
3501
|
+
step-stalled, other escalations, refusals) → failed. The detail also carries `startedAt` and `endedAt` (the last
|
|
3502
|
+
`job-end` timestamp, null while live or died). `GET /api/imports` rows and `GET /api/imports/:id` carry `runStatus`
|
|
3503
|
+
(passed, or null when the bundle shows no green) and `runReason` (`goal met`). The Ended block's lines for died and
|
|
3504
|
+
stopped runs are `no ending recorded` and `you pressed Stop` (each `— resume` when Resume is offered).
|
|
3505
|
+
**Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
|
|
3506
|
+
`checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
|
|
3507
|
+
(blank = the signed caps). `404` unknown run; `409` when `resumePlanFor` says the engine would
|
|
3508
|
+
refuse (`Resume is not available for this run (why).`); `400` for a bad cap, a money cap at or below what is already spent (or a time cap at or below the time
|
|
3509
|
+
used) — refused at $0 before any write or spawn — or the monthly $
|
|
3510
|
+
limit (same library check and text as Sign & run). If a cap changed, the new spec is written as
|
|
3511
|
+
`resolved-spec-r<k>.json` beside the original and its hash is approved; the signed spec the run
|
|
3512
|
+
started under is never overwritten, and a run resumed under `r<k>` is resumed again under the
|
|
3513
|
+
spec whose hash its `job-start` carries. The spawn is `setsid --wait systemd-inhibit node
|
|
3514
|
+
bin/bareloop.mjs run-u --spec <spec> --resume <runid> --approve <hash>` (array argv, detached,
|
|
3515
|
+
log `resume-<runid>-<ms>.log` beside the spec; `--draft-spent-usd`/`--draft-spend-incomplete`
|
|
3516
|
+
ride along from the run's `job-start`, so the one cap still covers drafting). `--wait` hands back
|
|
3517
|
+
the engine's own exit code: an exit inside the settle window (6s) is the engine's own refusal and
|
|
3518
|
+
the log tail is returned as `error` verbatim; otherwise `{ok:true, runid, specHash, capsChanged,
|
|
3519
|
+
log}`. The run keeps its runid and its one list row: the resumed leg appends to the same spine (P5-R,
|
|
3520
|
+
`leg-start` in the run list), it never appears as a new run. The page's Resume button opens the
|
|
3521
|
+
caps form (prefilled, "spent so far") and `Sign & resume` is the human click.
|
|
3522
|
+
**Stop (item 5)** → `POST /api/runs/:runid/stop` (`src/panel/runroutes.js`, `checkHumanGuard`, no body).
|
|
3523
|
+
It writes the run's STOP REQUEST, a file `<spine>.stop` (`stopFilePath(spine)`, `src/legs.js` — the one
|
|
3524
|
+
spelling), and signals nothing. `404` unknown run; `409` when the run's LATEST leg is not live
|
|
3525
|
+
(`runIsAlive`) or its spine does not exist yet (still starting). The ENGINE half is library code, so
|
|
3526
|
+
CLI runs honour the file too (no new CLI command): `runJob`/`runPlan` take `stopFile`, and `run-u`/the
|
|
3527
|
+
panel's spawn pass `<spine>.stop`. The seam is the ROUND BOUNDARY of a step worker — where the money cap binds
|
|
3528
|
+
(`src/planrun.js`, the `metered` round callback): file present = delete it, emit `stop-requested
|
|
3529
|
+
{phase, step, iteration, round}`, end the Loop after that round; `ask` then throws category `stopped`
|
|
3530
|
+
and the step loop files it as job-end outcome `stopped` the way a mid-step cap-halt is filed
|
|
3531
|
+
(`spendComplete` exact — nothing is in flight at a round boundary). One owner, one check: a one-step run
|
|
3532
|
+
and a long step both stop within one turn. Resume re-enters that step. The request file is cleared three times: when honoured, when the
|
|
3533
|
+
leg ends (`finally`), and at the start of the next leg (a click that raced a leg's end never stops a
|
|
3534
|
+
resume). `stopped` is in `CHECKPOINT_OUTCOMES`: resumable like a `cap-halt`, the same run continues as a
|
|
3535
|
+
new leg at the next step. `GET /api/runs/:runid` carries `live` and `stopping` (a stop request is on
|
|
3536
|
+
disk and the leg is live); Ended row `stopped` = "You stopped it." / "Resume." / `[Resume]`, card line
|
|
3537
|
+
`stopped — resume`, Audit divider `stopped: you stopped it · resumed <when>`. The page shows `[Stop]`
|
|
3538
|
+
in the Run tab's action area while live, "stopping after this turn…" after the click, and the same
|
|
3539
|
+
`[Resume]` there once the engine would accept one. The seam reads the file in STEP workers only
|
|
3540
|
+
(`phase` `step:<id>`): the scout, plan and close-fix rounds never read it, so a Stop clicked while the run
|
|
3541
|
+
is in one of those phases is not honoured until a step round runs again, and is otherwise cleared when
|
|
3542
|
+
the leg ends — the money and time caps still bind throughout.
|
|
3543
|
+
**Reuse workflow (replaces P5 item 3's Start from this, 2026-10-03)** — a button on GREEN runs only (the Run tab's
|
|
3544
|
+
action row and the Ended block; `ended.actions` carries `{id:'reuse', label:'Reuse workflow'}` on green and
|
|
3545
|
+
green-with-destination-refused rows, and on no other: a red row's next line is "Change the job: Clear the card and draft a new one.", and
|
|
3546
|
+
stopped/capped/died rows offer `resume` only) and on every imported job. It opens the Chat tab's New job card
|
|
3547
|
+
filled from the SIGNED job: a NEW run (new runid), never a rerun, nothing drafted ($0). **Only five boxes are
|
|
3548
|
+
open — Source, Destination, Model, $ cap, Time cap**; every other box (check type, model, job name, goal, success,
|
|
3549
|
+
guardrails, judge examples) is the signed workflow, greyed, and the SERVER refuses a start that changed one
|
|
3550
|
+
(`400 "<Field> is locked on a reused workflow — Clear the card to change it"`; the page is never trusted).
|
|
3551
|
+
Changing a locked box is Clear (the Chat card's top-right button), which drafts. Routes, behind `checkHumanGuard`:
|
|
3552
|
+
`GET /api/author/start-from?runid=<id>` or `?import=<id>` → `{ok, origin, card, from:'signed job'|'imported job',
|
|
3553
|
+
note, locked:[…], open:['source','destination','model','capUsd','maxWallMs'], specHash, workflowKey, trackRecord:{runs,
|
|
3554
|
+
green, notGreen, live, finished, avgSpendUsd, avgWallMs}, line}` (`404` unknown, `409` with an `error` when
|
|
3555
|
+
there is nothing to copy: no log, the run's signed job is not on disk, or an imported job that changed, lost its
|
|
3556
|
+
dependency or no longer matches its signed bytes); `POST /api/author/start` accepts `startFrom: <runid>` or
|
|
3557
|
+
`startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}`. The old
|
|
3558
|
+
`POST /api/author/start-from-check` and the "Changed — new job" mode are gone. **Identity, two keys:** the
|
|
3559
|
+
signature hash `jobSpecHash` is unchanged (it covers the caps and the write fence — what the person signs);
|
|
3560
|
+
`workflowKey(spec)` (`src/job.js`, not exported from the package root) is a sha256 over the signed spec WITHOUT
|
|
3561
|
+
`source`, `destination`, `writeScope`, `budgetUsd` and `maxWallMs` (`REUSE_OPEN_SPEC_FIELDS`) AND without `provider`,
|
|
3562
|
+
`baseUrl` and `model` (`WORKFLOW_KEY_IDENTITY_FIELDS`; `jobSpecHash` still covers all of them). A reuse copies the
|
|
3563
|
+
origin's signed spec, sets only `writeScope` (from Destination), `budgetUsd`, `maxWallMs` and the worker from the
|
|
3564
|
+
chosen Model Name (`buildReuseSpec(origin, card, rows)`: `provider`; `baseUrl` when the row has one; `model` only
|
|
3565
|
+
when the Name is not the provider's default tier — spelled as authoring spells it; a RUBRIC origin with no `judge`
|
|
3566
|
+
first gets `judge` = its own resolved judge identity so the judge never follows the worker; an explicit judge is
|
|
3567
|
+
kept; deterministic jobs get none), and signs under a
|
|
3568
|
+
NEW `jobSpecHash` with the SAME `workflowKey` (the session refuses a spec whose key moved). Locked boxes carry the
|
|
3569
|
+
signed values read by the Job tab's own readers. **Estimate line** above the card: "Same job — G green · N not
|
|
3570
|
+
green · about $X and M min a run", from every LISTED run whose signed spec has the same `workflowKey` AND ran the same worker (provider + baseUrl + model) as the Model box now names — `GET /api/author/start-from?…&model=<Name>` recomputes it when the box changes; none = "Same job — no runs yet on this model" (one run =
|
|
3571
|
+
one entry via `legsOf`; the averages read FINISHED runs with exact figures — spend from `chainSpend`, working time
|
|
3572
|
+
from `legsWallMs`, resume gaps excluded); unknown is said ("no finished run yet to price or time", "cost not
|
|
3573
|
+
recorded", "time not recorded"), never $0 or 0 min; no registry, no plan handover. **An imported job** (its provider is not forced: the chosen Settings row is the worker; a card with no matching row opens with no Name picked) is
|
|
3574
|
+
re-verified at the moment of reuse — `readBundle` (the bundle hash covers every close script), `checkBundleDeps`,
|
|
3575
|
+
and every close stage's signed sha256 against the bytes on disk over the spec with `$BARELOOP_BUNDLE` resolved to
|
|
3576
|
+
the folder — then reused the same way; the close scripts stay in the verified folder (the engine re-verifies
|
|
3577
|
+
their bytes at run start and before every close run). An operator-written command close has no declaration for
|
|
3578
|
+
`prepareSigning` to ground (it refuses one by design), so that session's gate is the byte check, and the hash the
|
|
3579
|
+
person signs at `[Sign & run]` is the spec's own. The ONE wide card button (below the message box; wording from `mainButtonFor`: Start drafting / Sign & run / Check again / Send / disabled) reads `Sign & run` on a Reuse card (one click: the page signs the
|
|
3580
|
+
prepared hash as soon as the session reports `prepared`; the server re-checks it). `card.json` (the form text)
|
|
3581
|
+
is still written beside `resolved-spec.json` when a session reaches `prepared`.
|
|
3582
|
+
**The progress list (2026-10-04)** — `GET /api/author/:id` state carries `steps: [{id, label, status:
|
|
3583
|
+
'running'|'done'|'failed', detail}]`, one entry per pipeline step in first-seen order, updated in place (ids/labels:
|
|
3584
|
+
`STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A refusal sets
|
|
3585
|
+
`state.error` and fails exactly one step with the same text as its `detail`; pipeline text is never a chat message
|
|
3586
|
+
(`messages` holds person/model turns only). Two last steps, `hash` ("generating hash", detail `spec hash <full hash>`, done at sign-prepare) and `signed` ("signed hash", pushed by `signRun` when the sign is accepted); the old "SIGNING PREPARED" / "signed — … run starting detached" thread bubbles are gone. The page draws each step's `detail` on its own `> ` line under the step. Destination follows the Source: for a repo source it is the write
|
|
3587
|
+
fence (relative `writeScope` globs, never proven as a directory); for a folder source an absolute directory. Both
|
|
3588
|
+
are refused at $0, before any model call (the same rule on a fresh and a Reuse card): an ABSOLUTE
|
|
3589
|
+
Destination on a repo source → `Destination must be a path inside the repo, like src/digest.js` (before the
|
|
3590
|
+
copy); a source that is not a git repo → a plain refusal worded by the kind the source door recorded (a single
|
|
3591
|
+
file: "Source must be the repo folder — put the file in Destination"; a plain folder or a URL: "not a code
|
|
3592
|
+
project (no git repo found)").
|
|
3593
|
+
**Import, read only (item 4)** — `src/panel/importroutes.js`. The Workflows toolbar's `[Import]` opens a folder
|
|
3594
|
+
browser plus a paste box; an imported job is an exported bundle folder the person can LOOK at: nothing runs,
|
|
3595
|
+
nothing is signed, the folder is never written. Routes, all behind `checkHumanGuard` (token + own address —
|
|
3596
|
+
new disk exposure, so the strict guard, not the Host guard the plain GETs use): `GET /api/fs/list?path=`
|
|
3597
|
+
→ `{ok, requested, path, parent, bundle, entries:[{name, path, bundle:true}], truncated, stopped, visited, depth}`
|
|
3598
|
+
(job BUNDLES only — a folder holding a regular file `manifest.json` — found by searching DOWN from `path` at most
|
|
3599
|
+
`depth` = 3 folders; `name` is the path relative to `path`, `path` the absolute one; symlinks never followed,
|
|
3600
|
+
`node_modules`/`.git`/dot-folders skipped, a bundle is never descended into, unreadable folders skipped silently;
|
|
3601
|
+
capped at 5000 folders visited (`stopped`) and 200 bundles (`truncated`); starts at the home folder when `path` is blank);
|
|
3602
|
+
`POST /api/imports {path}` → `{ok, id, job, dir}` (`readBundle` must come back clean — a plain or tampered
|
|
3603
|
+
folder is `400` and records nothing); `GET /api/imports` (the list, every bundle RE-READ now) and
|
|
3604
|
+
`GET /api/imports/:id` (one view). Imported list: `<home>/imports.jsonl`, one `{at, dir, job, bundleHash}` line
|
|
3605
|
+
per FOLDER (a re-import replaces that folder's line; a file already holding duplicates lists once per real path, newest wins; mode 0600 in the 0700 config home); `id` = the first 12 hex of
|
|
3606
|
+
sha256(real path). **Path safety:** the path is expanded (`~`, `~/…` only), refused if it holds a NUL byte, is
|
|
3607
|
+
over 4096 bytes or is not absolute, then `resolve`d and `realpath`ed — the REAL folder is listed and recorded
|
|
3608
|
+
and the response says which it chose (`path` beside `requested`); a non-directory is refused; a SYMLINK inside the
|
|
3609
|
+
search is never listed and never followed; names only, never file contents. **Status, re-read on every list/view:** `ok`, `changed` ("changed since import",
|
|
3610
|
+
red: a different/tampered/unreadable bundle) or `missing` ("folder not found"). The view carries goal,
|
|
3611
|
+
`checkType`, `success` (the close stage names), `guardrails`, `budgetUsd`/`maxWallMs`, `model`, the history
|
|
3612
|
+
the bundle shipped (`history:{greens,reds,total,recent}`, read from the bridges' history rows) and
|
|
3613
|
+
`approved`/`approvedText` — approval on THIS machine is `verifyBlessing` (the first green run here writes
|
|
3614
|
+
`blessing.json`): approved / not approved yet / stale. The row reads `imported · view only`; the view opens in
|
|
3615
|
+
the same Run / Audit / Job tabs as any run (Audit: "no log — this job ran on another machine"), and its one
|
|
3616
|
+
button, `[Reuse workflow]` in the Run tab's top action row, reuses the bundle's signed job as described above.
|
|
3617
|
+
`GET /api/imports/:id` also carries `runsHere` (how many `runs/<runid>/` folders in the bundle hold a readable
|
|
3618
|
+
`spine.jsonl`) and `run`, what the Run tab shows below the imported facts: `{kind:'spine', id, runid, at}` (the
|
|
3619
|
+
newest run in the bundle's own `runs/` that ended green; `id` is `<importId>~<runid>`), else `{kind:'bridge', runid,
|
|
3620
|
+
at, detail}` (the bridge's green version rendered as a run detail, fields the bridge does not carry left `null`),
|
|
3621
|
+
else `{kind:'none'}`; never throws. **An imported bundle's run is served read only** through the ordinary run routes
|
|
3622
|
+
under that composite id: `GET /api/runs/<importId>~<runid>` (detail), `…/audit` and `…/rounds` (and `…/job`) read
|
|
3623
|
+
the spine inside the bundle (`importrun.js` `safeSpinePath`: a checked name, `lstat`ed, a link never followed); an
|
|
3624
|
+
unknown import or run is `404`. Such a run is never in `GET /api/runs`, never live (`live`/`stopping` are false,
|
|
3625
|
+
no pid), never resumable or stoppable from the panel.
|
|
3626
|
+
**Run-card fixes (item 6)** → a listed run whose runner is alive but whose spine is not written
|
|
3627
|
+
yet reads `starting: true` (list AND detail; glyph `▶`, every figure null, `fileMissing:false`) —
|
|
3628
|
+
never `file missing`; a row whose runner is gone and whose spine is absent stays `fileMissing`.
|
|
3629
|
+
`GET /api/runs` rows now carry `spendFloorUsd`/`wallFloorMs` for a LIVE spine too (previously died
|
|
3630
|
+
only; both `null` once a `job-end` exists), the same `deriveDeath` floors the detail carries, so a
|
|
3631
|
+
live card reads "$X so far · running Ym" exactly like the right pane.
|
|
3632
|
+
|
|
3408
3633
|
- **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
|
|
3409
3634
|
(the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
|
|
3410
3635
|
U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
|
|
@@ -3440,7 +3665,7 @@ got none, and a run started from the menu skipped the limit). All three are disp
|
|
|
3440
3665
|
mechanically, $0, via `prepareSource`/`proveDestination`), then the picked class's frozen
|
|
3441
3666
|
question set (`questionsFor`/`requiredAnswersFor`, `src/authorjob.js`), then the operator's
|
|
3442
3667
|
own job name/budget/wall. On success it writes `answers.json`/`specdraft.json` under `--out`
|
|
3443
|
-
and either offers to spawn `scripts/run-author.mjs
|
|
3668
|
+
and either offers to spawn the packaged `bin/bareloop.mjs author` (same flags as `scripts/run-author.mjs`, which the package does not ship; only when a clean key is present and no
|
|
3444
3669
|
install gap remains — default answer is no) or prints the exact command to run it later.
|
|
3445
3670
|
Because this module reads a real TTY/piped stdin, `src/cli.js` hands it `stdin`/`stdout`/
|
|
3446
3671
|
`stderr` (the raw streams, not the wrapped `out`/`err` line-functions every other command
|
|
@@ -3502,7 +3727,8 @@ and leaves the worktree in place until the human removes it.
|
|
|
3502
3727
|
|
|
3503
3728
|
**`history.jsonl` row shape** — one JSON object per line, one line per `bareloop run` on this
|
|
3504
3729
|
machine: `{ runid, at, outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree,
|
|
3505
|
-
branch, bundleHash, approveHash }
|
|
3730
|
+
branch, bundleHash, approveHash, leg? }` (`leg` only from a resumed run's second leg: the same `runid`,
|
|
3731
|
+
one row per leg). `bundleHash` is the bundle's own signed manifest hash
|
|
3506
3732
|
(unchanged across tightened runs); `approveHash` is the hash of the actual, possibly-
|
|
3507
3733
|
tightened, `$BARELOOP_BUNDLE`-resolved spec that run executed — the two are recorded side
|
|
3508
3734
|
by side so a tightened run's real approval can never be confused with the bundle's headline
|
|
@@ -3559,7 +3785,7 @@ provider its own row, for the worker's provider the row on the worker's endpoint
|
|
|
3559
3785
|
(The judge's KEY still follows the worker's row, so with two rows on one endpoint key and price can differ.) A priced round is stamped `rateSource:'caller'` (VOUCHED, see `spendProvenance`); with
|
|
3560
3786
|
no price it stays the built-in guess, as before. The run preview and the run tail print
|
|
3561
3787
|
`price yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)` when a price is set, and
|
|
3562
|
-
nothing when none is
|
|
3788
|
+
nothing when none is; a 0 / 0 price is accepted but the line then ends `— WARNING: this price is $0, so spend and limits read $0`. **The price is yours, and it sets what the cap means:** the cap and the halts run
|
|
3563
3789
|
on the dollars your price produces, so a price set too low weakens the cap (a run can do far more
|
|
3564
3790
|
than you meant before it halts). When a vendor lists two prices (for example a cache-hit and a
|
|
3565
3791
|
cache-miss input rate) enter the higher one. bare-agent applies its own cache multipliers (read 0.1×,
|
|
@@ -3570,13 +3796,23 @@ re-priced** (a spine and the run list keep the dollars they booked). Not covered
|
|
|
3570
3796
|
resolves no key row.
|
|
3571
3797
|
|
|
3572
3798
|
**Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
|
|
3573
|
-
atLeast }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
|
|
3574
|
-
limit)
|
|
3799
|
+
atLeast, heldUsd, heldRuns }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
|
|
3800
|
+
(monthly limit)`, and when run(s) in progress hold money beyond what they have spent, the same line
|
|
3801
|
+
reads `Max $4.00 (monthly limit, $5.95 held by a run in progress)` (`runs` when more than one). It is called at the run-start seam in `src/userrun.js` `execute` (before any token
|
|
3575
3802
|
spends; the CLI prints it to stderr, exit 2) and at the panel's Sign & run (`signRun` in
|
|
3576
3803
|
`src/panel/authorroutes.js`, refused server-side whatever the page shows). The page only echoes the
|
|
3577
3804
|
same text under `#jf-cap-money` via `GET /api/author/monthly-check?cap=`. "This month" is the local
|
|
3578
3805
|
calendar month; a died or incomplete-spend run makes the total an "at least" figure.
|
|
3579
3806
|
|
|
3807
|
+
*Drafting spend that never became a run* (2026-10-05, F213). A panel authoring session writes
|
|
3808
|
+
`<session dir>/draft-spend.json` (`{ sessionId, spentUsd, spendComplete, provider, baseUrl, model,
|
|
3809
|
+
startedAt, updatedAt }`, atomic) after every metered call, so an abandoned, refused or restarted session's
|
|
3810
|
+
drafting money is not lost. `readLegs` (hence `monthSpend`, `spendSummary`, `checkMonthlyRoom` and the Money
|
|
3811
|
+
tab) adds every `<home>/panel-sessions/<id>/` that has one and whose folder holds no listed run's `spine` or
|
|
3812
|
+
`patient` (a signed run already carries its drafting spend via `--draft-spent-usd`; no double count). It lands in
|
|
3813
|
+
the month of `startedAt` (the first call), under the provider row it was drafted on; `spendComplete: false` or an
|
|
3814
|
+
unreadable file makes the figure an "at least". It is not a run: no wall, not in the `runs` count.
|
|
3815
|
+
|
|
3580
3816
|
*Hold until done.* A run CLAIMS the limit when it starts and holds it until it is done — no lock file,
|
|
3581
3817
|
records only. `claimRun` (`src/monthly.js`) writes the run's row FIRST (it carries its `pid` and
|
|
3582
3818
|
`capUsd`), then reads `runs.jsonl`: earlier claims come first, and only the claims ABOVE its own row count
|
|
@@ -3584,17 +3820,17 @@ records only. `claimRun` (`src/monthly.js`) writes the run's row FIRST (it carri
|
|
|
3584
3820
|
its pid is a live bareloop runner (`isLiveRunner`, `src/runlist.js`, also what `--resume` uses to refuse a
|
|
3585
3821
|
resume against a live run: the process is a node executable with a `bareloop`/`bareloop.mjs`/`run-u.mjs`/
|
|
3586
3822
|
`u-watchdog.mjs` argument, or is itself named so — a recycled pid running some other program never holds a
|
|
3587
|
-
cap, though a recycled node program with such an argument still would), however quiet its spine; a claim whose process is gone is closed by whoever
|
|
3823
|
+
cap, though a recycled node program with such an argument still would), however quiet its spine (`runIsAlive`, `src/runlist.js`, is the one "is this run alive" rule: the panel's [▶] / [?] glyph reads it too; `--resume` also refuses on the pid in the predecessor's own run-list row, not only the watchdog's record); a claim whose process is gone is closed by whoever
|
|
3588
3824
|
finds it, with a note (`{runid, type:'settled', by:<finder>, reason:'process gone', spentUsd:<floor>}`) and
|
|
3589
3825
|
counts its real spend. If the sum fits, the run goes; if not, it appends `{type:'released',
|
|
3590
3826
|
reason:'refused'}` and refuses with the exact text, nothing spent. A run whose row is listed (claimed, or with no limit set) that then exits at $0 before its spine has a first record (a refused
|
|
3591
|
-
`--resume` patient, a failing tree setup) appends `{type:'released', reason:'not started'}` too, so it leaves no row. A row that cannot be written, or an
|
|
3827
|
+
`--resume` patient, a failing tree setup) appends `{type:'released', reason:'not started'}` too, so it leaves no row; so does a $0 refusal after the spine exists (`SOURCE-MANIFEST-RED`, `DESTINATION-RED`, `CLOSE-TIMING-RED`). A row that cannot be written, or an
|
|
3592
3828
|
unreadable config, refuses too — never "no limit". When a run ends it appends its own
|
|
3593
3829
|
`{runid, type:'settled', by:<runid>, spentUsd, spendComplete, at}`. Two runs may close the same dead claim;
|
|
3594
3830
|
the FIRST note for a runid is authoritative. `readRunList` folds these entries out of the run rows
|
|
3595
3831
|
(`events`; a `released` claim's row is not listed). Month totals still read real spend off the spines. An
|
|
3596
3832
|
older row with no `pid` keeps the spine-mtime rule (`DIED_MTIME_MS`: a spine written to within it is in
|
|
3597
|
-
flight, at its leg cap). A run refused because an earlier claim was itself later refused is the accepted
|
|
3833
|
+
flight, at its leg cap). The refusal check parses only the spines of runs listed in the current local month (plus any claim still held by a live runner); the all-time readers still read every run. A run refused because an earlier claim was itself later refused is the accepted
|
|
3598
3834
|
safe-side cost of doing without a lock.
|
|
3599
3835
|
Only the refusal reserves the cap (`monthSpend().reservedUsd`); the Money tab's month figure is
|
|
3600
3836
|
real spend.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/bundlerun.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './bundle.js';
|
|
24
24
|
import { jobSpecHash } from './job.js';
|
|
25
25
|
import { parseJsonl } from './replayio.js';
|
|
26
|
+
import { legsOf } from './legs.js';
|
|
26
27
|
import { startRun, resumeRun } from './userrun.js';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -117,7 +118,9 @@ export async function bundleMain(args, {
|
|
|
117
118
|
// after its own $0 refusals, so a refusal leaves no worktree behind. A RESUME is the one
|
|
118
119
|
// exception: it re-enters the SAME worktree its halted run left (recorded in that run's
|
|
119
120
|
// `run.json`), and a gone tree is a stop, never a silent fresh start.
|
|
120
|
-
|
|
121
|
+
// P5-R (hamr 2026-10-02: one run, one id, one file): a RESUME is the same run — same runid, same run dir, same
|
|
122
|
+
// spine; the engine appends the new leg to it. Only a fresh run mints an id.
|
|
123
|
+
const runid = resumeArg !== undefined ? resumeArg : now().toString(36);
|
|
121
124
|
/** @type {string} */
|
|
122
125
|
let repo;
|
|
123
126
|
/** @type {string} */
|
|
@@ -126,6 +129,8 @@ export async function bundleMain(args, {
|
|
|
126
129
|
let worktree;
|
|
127
130
|
/** @type {string|null} */
|
|
128
131
|
let deadSpine = null;
|
|
132
|
+
/** the run's own first-leg stamp, kept when a later leg rewrites `run.json` (it is the run's start, not the leg's) @type {string|null} */
|
|
133
|
+
let firstAt = null;
|
|
129
134
|
if (resumeArg !== undefined) {
|
|
130
135
|
if (!/^[a-z0-9]+$/.test(resumeArg)) { err(`--resume ${JSON.stringify(resumeArg)} is not a run id of this bundle`); return 1; }
|
|
131
136
|
const deadRunJson = join(bundleDir, 'runs', resumeArg, 'run.json');
|
|
@@ -150,6 +155,7 @@ export async function bundleMain(args, {
|
|
|
150
155
|
repo = rec.repo;
|
|
151
156
|
seed = rec.seed;
|
|
152
157
|
worktree = rec.worktree;
|
|
158
|
+
if (typeof rec.at === 'string') firstAt = rec.at;
|
|
153
159
|
deadSpine = join(bundleDir, 'runs', resumeArg, 'spine.jsonl');
|
|
154
160
|
} else {
|
|
155
161
|
repo = resolve(cwd, /** @type {string} */ (repoArg));
|
|
@@ -189,7 +195,7 @@ export async function bundleMain(args, {
|
|
|
189
195
|
}
|
|
190
196
|
mkdirSync(runDir, { recursive: true });
|
|
191
197
|
writeFileSync(join(runDir, 'run.json'), `${JSON.stringify({
|
|
192
|
-
runid, worktree, seed, repo, at: new Date(now()).toISOString(),
|
|
198
|
+
runid, worktree, seed, repo, at: firstAt ?? new Date(now()).toISOString(),
|
|
193
199
|
}, null, 2)}\n`);
|
|
194
200
|
};
|
|
195
201
|
|
|
@@ -227,7 +233,9 @@ export async function bundleMain(args, {
|
|
|
227
233
|
// 7. history + (on a first green) the blessing — off THIS run's own spine, tolerant of
|
|
228
234
|
// a torn line. No job-end (a $0 refusal, a crash) means no history row, as ever.
|
|
229
235
|
const events = existsSync(spineFile) ? parseJsonl(spineFile).records : [];
|
|
230
|
-
|
|
236
|
+
// the LATEST leg's terminal: an earlier leg's job-end on the same file is a row already written for it
|
|
237
|
+
const legs = legsOf(events);
|
|
238
|
+
const je = legs.at(-1)?.jobEnd ?? null;
|
|
231
239
|
if (!je) return engineCode;
|
|
232
240
|
const outcome = je.outcome;
|
|
233
241
|
const spentUsd = je.spentUsd ?? null;
|
|
@@ -246,7 +254,8 @@ export async function bundleMain(args, {
|
|
|
246
254
|
branch,
|
|
247
255
|
bundleHash: manifest.bundleHash,
|
|
248
256
|
approveHash,
|
|
249
|
-
|
|
257
|
+
// one row per LEG of the one run; the first leg's row keeps the shape it always had
|
|
258
|
+
...(legs.length > 1 ? { leg: legs.length } : {}),
|
|
250
259
|
});
|
|
251
260
|
if (outcome === 'green' && !bundle.blessing) {
|
|
252
261
|
bless(bundleDir, {
|