bareloop 0.34.0 → 0.36.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bareloop.context.md +276 -20
  3. package/package.json +1 -1
  4. package/src/authorrun.js +3 -2
  5. package/src/bundlerun.js +17 -12
  6. package/src/draftspend.js +197 -0
  7. package/src/exits.js +10 -1
  8. package/src/index.js +3 -3
  9. package/src/interviewrun.js +7 -7
  10. package/src/job.js +37 -0
  11. package/src/judged.js +2 -2
  12. package/src/kinds.js +2 -1
  13. package/src/ledger.js +101 -5
  14. package/src/legs.js +171 -0
  15. package/src/monthly.js +111 -72
  16. package/src/movefile.js +22 -0
  17. package/src/npminstall.js +55 -0
  18. package/src/panel/authorroutes.js +84 -14
  19. package/src/panel/authorsession.js +490 -125
  20. package/src/panel/importroutes.js +359 -0
  21. package/src/panel/importrun.js +203 -0
  22. package/src/panel/index.html +1194 -213
  23. package/src/panel/runroutes.js +219 -0
  24. package/src/panel/server.js +964 -79
  25. package/src/panel/settingsroutes.js +52 -2
  26. package/src/panel/status.js +51 -0
  27. package/src/planrun.js +60 -26
  28. package/src/providerrows.js +17 -0
  29. package/src/ralph.js +4 -1
  30. package/src/replay.js +139 -19
  31. package/src/replayio.js +11 -4
  32. package/src/reuse.js +20 -3
  33. package/src/run.js +6 -3
  34. package/src/runlist.js +75 -7
  35. package/src/source.js +43 -4
  36. package/src/text.js +20 -0
  37. package/src/u-patient.js +3 -2
  38. package/src/u-readout.js +9 -1
  39. package/src/u-watchdog.mjs +4 -2
  40. package/src/userrun.js +192 -65
  41. package/src/worktree.js +120 -0
  42. package/types/draftspend.d.ts +141 -0
  43. package/types/job.d.ts +25 -0
  44. package/types/ledger.d.ts +44 -0
  45. package/types/legs.d.ts +103 -0
  46. package/types/monthly.d.ts +23 -18
  47. package/types/movefile.d.ts +9 -0
  48. package/types/npminstall.d.ts +30 -0
  49. package/types/panel/authorroutes.d.ts +23 -1
  50. package/types/panel/authorsession.d.ts +110 -2
  51. package/types/panel/importroutes.d.ts +129 -0
  52. package/types/panel/importrun.d.ts +77 -0
  53. package/types/panel/runroutes.d.ts +32 -0
  54. package/types/panel/server.d.ts +266 -2
  55. package/types/panel/settingsroutes.d.ts +6 -1
  56. package/types/panel/status.d.ts +51 -0
  57. package/types/planrun.d.ts +13 -21
  58. package/types/providerrows.d.ts +12 -0
  59. package/types/ralph.d.ts +19 -0
  60. package/types/replay.d.ts +25 -1
  61. package/types/replayio.d.ts +2 -2
  62. package/types/reuse.d.ts +5 -0
  63. package/types/run.d.ts +6 -2
  64. package/types/runlist.d.ts +52 -2
  65. package/types/source.d.ts +13 -5
  66. package/types/text.d.ts +19 -0
  67. package/types/userrun.d.ts +10 -0
  68. package/types/worktree.d.ts +48 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,64 @@ 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.36.0] - 2026-10-06
9
+
10
+ ### Added
11
+
12
+ - **Repo jobs run in a git worktree in the person's own repo.** A panel drafting session makes `<repo>/.bareloop/wt/<session>` (detached at HEAD, hidden through the repo's private exclude, with a notice for uncommitted changes) instead of copying the source. `git worktree add` and the green-end commit run none of the person's git hooks. `node_modules/` is hidden the same way as `/.bareloop/`, so installed packages never read as worker writes. A GREEN run commits its work to the work branch and removes the folder (kept only while a review door is open); any other end keeps it. The Ended block gives the `git merge` / `git branch -D` text on green and names the folder otherwise; Resume is withdrawn once the folder is gone. The bundle door shares the one worktree-making module.
13
+ - **bareloop installs npm dependencies itself.** For an npm lock file, bareloop runs `npm ci --ignore-scripts` at $0, before any token, with a secret-stripped environment. Any failure, or another lock file (yarn, pnpm), falls back to the install-needed wait. `missingDependencies` now carries `dir` and `lockFile`. The panel step reads "installing packages" while it runs and "Installed packages (npm ci)." on success.
14
+ - **Run names `job (run-N)`.** Runs are numbered per job, oldest first (`runNo` in the run list and run detail); rows, cards, the workflow row and the header use the name. The real run id stays in the Resume and Reuse texts. Runs search also finds `run-N` and `job (run-N)`.
15
+ - **Settings: price per key row.** The Providers table takes In $/1M and Out $/1M per key (blank = not set; a half-set price is refused; the price posts once both boxes are filled or both are empty). Columns are Key, Name, API shape, Base URL, Test, Tokens used, Balance, In $/1M, Out $/1M.
16
+ - **Settings: Open keys folder button** (`POST /api/settings/open-keys-folder`) replaces the keys path text; the path shows only when the folder cannot be opened.
17
+ - **Drafting part.** A panel-authored run serves its drafting as the first part on the Run map, its card and the Audit grouped view, with a per-step table (time, calls, model, cost, total) read from a per-session `draft-log.jsonl`.
18
+ - **Runs list cards are two lines**: workflow card `job (run-N) word / check, cost, wall, date`; sub-card `(run-N) word / cost, wall, date`.
19
+
20
+ ### Fixed
21
+
22
+ - A gate-audit move across devices no longer crashes a worktree run: `moveFile` falls back to copy and unlink on `EXDEV` (the workdir can sit on another drive than the spine dir).
23
+ - A green worktree run always commits its work to the branch, including door-open and leak greens, and does so right after the gate-audit move, before any later readout step can throw.
24
+ - An already-green worktree run removes its folder (and commits a resumed leg's uncommitted work) instead of leaving it behind.
25
+ - Panel: a price box typed alone no longer flashes the half-set error; card textareas auto-grow as you type.
26
+ - Tests no longer write settled rows into the real run list (`cli.test.js` passes `keysHome` with `runlistHome`). Test-only.
27
+
28
+ ### Changed
29
+
30
+ - A secret already in the person's own repo no longer refuses a worktree repo source: it is masked wherever bareloop records it. A `.env` file by name is still refused.
31
+ - The link-login item is dropped; the panel's token-in-page model is unchanged.
32
+ - The progress step label "copying source" reads "making worktree".
33
+ - `prepareSource`'s manifest type is `Record<string, any>` (types only).
34
+
35
+ ## [0.35.0] - 2026-10-05
36
+
37
+ ### Added
38
+
39
+ - **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.
40
+ - **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...".
41
+ - **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.
42
+ - **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.
43
+ - **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."
44
+ - **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.
45
+ - **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.
46
+ - **Monthly claims.** Settled per leg: a resumed run stays one listed row and only the leg's claim is released.
47
+ - **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.
48
+ - **`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.
49
+ - **`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.
50
+ - **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.
51
+
52
+ ### Fixed
53
+
54
+ - **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.
55
+ - **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.
56
+ - **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).
57
+ - 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.
58
+ - 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.
59
+ - 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.
60
+
61
+ ### Changed
62
+
63
+ - 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.
64
+ - 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.
65
+
8
66
  ## [0.34.0] - 2026-10-01
9
67
 
10
68
  ### Fixed
@@ -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). `evalExits` is AND-only
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 worker round the API cut off mid-generation (`truncated:max_tokens`,
1384
- BA-6 — before which it laundered into a clean finish, F25): no verdict exists and the failed
1385
- round's spend is only partly known (F6). `cap-halt` is the wallet; `wall-halt` is the clock
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
@@ -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 run leg — spine.jsonl, gate-audit.jsonl, close/ (the
3001
- close's books) and run.json ({runid, worktree, seed, repo, at, resumedFrom?})
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`
@@ -3204,7 +3251,8 @@ key you want first (the panel picks the row by the Model menu).
3204
3251
  different from the recorded one is a stop, exit `1`, spending nothing; `--repo` is optional
3205
3252
  on a resume) and hands the engine `resumeRun` on that run's spine — the engine's own gates
3206
3253
  (checkpoint age, tree-at-seed, liveness) and spend fold apply, so `--budget` can never widen
3207
- a resume. The new leg gets its own `runs/<newid>/`. The resume command a halted leg prints carries that leg's own
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
3208
3256
  `--budget`/`--wall`, only if you passed them (a tightened ceiling never silently reverts to the signed
3209
3257
  one). After a MONEY halt the line does NOT repeat the exhausted `--budget` (chain spend is folded, so
3210
3258
  it would halt again at once); it prints `--budget <more than $<spent so far>, at most $<signed
@@ -3223,7 +3271,8 @@ key you want first (the panel picks the row by the Model menu).
3223
3271
  inputs); `--door`/`--decide`/`--review-door` are not exposed on `run`.
3224
3272
  8. `appendHistory` — one `history.jsonl` line off this leg's own `job-end`: `{ runid, at,
3225
3273
  outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash,
3226
- approveHash, resumedFrom? }` (the `bundleHash` ↔ `approveHash` pairing, POC fact 2).
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`).
3227
3276
  `spentUsd`/`spendComplete` are never fabricated as `0` when unknown. No `job-end` (a $0
3228
3277
  refusal, a crash) writes no row. A `green` outcome on a still-unblessed bundle also calls
3229
3278
  `bless(bundleDir, { bundleHash, runid, outcome, host })`.
@@ -3295,7 +3344,7 @@ key you want first (the panel picks the row by the Model menu).
3295
3344
  $0: no interview, no author, no run trigger, no key ever read. A row is `{ at, runid, job,
3296
3345
  spine, patient, via }` — `spine`/`patient` are always absolute paths (`patient: null` when
3297
3346
  not known, e.g. every `via:"backfill"` row: no spine record carries a run's workdir);
3298
- `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
3299
3348
  bundle path) each append their own row at run START, before the first paid call — a
3300
3349
  list-append failure is caught at both call sites and printed loudly to stderr
3301
3350
  (`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (…)`); the run itself
@@ -3314,7 +3363,12 @@ key you want first (the panel picks the row by the Model menu).
3314
3363
  list by job for the left pane's "Workflows" view and renders it flat for "History", both
3315
3364
  behind one `Runs` tab toggle; there is no separate `/api/workflows` endpoint, deleted in
3316
3365
  the P1 Runs-tab merge); `GET /api/runs/:runid` (the full replay — parts/steps, counters,
3317
- summary, model/provider/judge); `GET /api/runs/:runid/audit` (the gate-audit sidecar rows,
3366
+ summary, model/provider/judge). Every `GET /api/runs` row and the detail carry `runNo` (P6 item 7): runs of one job are numbered
3367
+ oldest `at` first by ONE server function (`runNumbers`), and the page names a run `job (run-N)`; the run id stays in the Audit
3368
+ tab and every command line. A run that came out of a panel drafting session also carries the DRAFTING part as the FIRST element of
3369
+ `detail.parts` (`kind:'drafting'`, built from the session's `draft-log.jsonl`, `src/draftspend.js`): every other index a reader
3370
+ is handed (`legDividers.beforePart`, the flat Audit rows' `partIndex`, the rounds endpoint's `part`) lives in that one shifted
3371
+ index space, and the drafting part has no rounds table (`rounds?part=0` is a 404). `GET /api/runs/:runid/audit` (the gate-audit sidecar rows,
3318
3372
  ts-windowed to this run alone even when the sidecar file is shared with other runs, plus
3319
3373
  the matching raw-log text windowed the same way — when one resolves); `GET
3320
3374
  /api/runs/:runid/rounds?part=&attempt=&offset=&limit=` (one attempt's rounds, lazily
@@ -3385,9 +3439,18 @@ key you want first (the panel picks the row by the Model menu).
3385
3439
  never a reimplementation of any gate. `GET /api/author/model-check?model=<id>` is the $0 readiness probe the job card runs
3386
3440
  before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
3387
3441
  completion; the page sees the key's NAME and a status word, never the value). `POST
3388
- /api/author/:id/check-deps` is the install-gap's "Check again" (`phase:'install-needed'`:
3442
+ /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'`:
3389
3443
  the session waits on the person's own install, then re-runs `missingDependencies` on the
3390
- same copy — bareloop never installs). `GET /api/author/:id` polls the session's state
3444
+ same worktree — the AGENT never installs; for a committed npm lock file the panel door first runs `npm ci --ignore-scripts` itself,
3445
+ $0, before any token (`src/npminstall.js`, credential env stripped), and only a failure, a timeout or another lock file lands here). `POST /api/author/:id/abandon` (the Chat card's **Abandon** button, 2026-10-05) ends a
3446
+ LIVE, unsigned session: phase becomes the terminal `abandoned` (releasing the one-at-a-time lock), money already booked
3447
+ stays booked (`draftSpentUsd` is never zeroed; a call in flight still books its usage), no NEW model call starts;
3448
+ `400` on a session that is not live, `404` unknown id, same guard as the other author routes. After `signed` the card's
3449
+ button reads **Clear** and only empties the card (a run is stopped from the Run tab). `GET /api/author/live` →
3450
+ `{ok, sessionId, state}` of the ONE live (non-terminal) session, or `{sessionId:null, state:null}`: a refreshed page
3451
+ re-attaches to it from the server's session map (nothing is stored page-side). A panel restart mints a new token and
3452
+ loses the in-memory sessions: the page then shows one "reload this page" line and stops the Chat poll (no auto-reload;
3453
+ drafting money already spent stays booked via `draft-spend.json`, below). `GET /api/author/:id` polls the session's state
3391
3454
  (phase, chat messages, cost, `revisesLeft`, `specHash` once prepared). `POST /api/author/
3392
3455
  :id/send {text}` answers whatever the confirm turn is currently asking (the `language` pick or a plan's
3393
3456
  own follow-up question — the old `worseThanBefore` ask is retired, 2026-09-28) — refused outright when the pending ask is the
@@ -3412,6 +3475,167 @@ key you want first (the panel picks the row by the Model menu).
3412
3475
  `signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
3413
3476
  `~/.config/bareloop/.env` keys-file loader, and Settings have landed (P4a/P4b — see "Settings" below).
3414
3477
 
3478
+ - **Panel P5 — the Ended block (`docs/product/PANEL-BUILD.md` Addendum 2026-10-02, item 1)** →
3479
+ `GET /api/runs/:runid` carries `ended` = `{reason, next, line, actions:[{id,label}]}` for every
3480
+ FINISHED run (green too; `null` while the run is live), and `GET /api/runs` carries `endedLine`
3481
+ (the card's one short line under the job name, e.g. `money cap — resume`). Both come from ONE
3482
+ code-owned function, `endedFor` (`src/panel/server.js`): fixed sentences keyed by the run's
3483
+ outcome, with only
3484
+ numbers and the engine's own recorded detail slotted in — never model text. The table:
3485
+ green/already-green → "Goal met." (a `destination-refused` record turns it into "…the output
3486
+ could not be delivered (detail)."); cap-halt → "Money cap reached ($X of $Y)." / "Raise the cap,
3487
+ then Resume."; wall-halt → "Time cap reached."; provider-red → "The model provider failed
3488
+ (detail)."; step-stalled → "A step stopped making progress."; plan-red/check-red/step-red/escalated
3489
+ → "Goal not met — the checks said no (last gap)."; close-red → "The check itself broke
3490
+ (instrument fault), not your goal."; `escalated` is a terminal and never resumable — its escalation of
3491
+ category `cap-halt` with `spend.strikes` is the STRIKE governor, read "The fix loop stopped improving
3492
+ (S of L tries, no check got better)." (card line `stopped improving`), and is the money cap ONLY when
3493
+ 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
3494
+ (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 `✗`) →
3495
+ "Stopped with no ending recorded (last thing it did: …)." `actions` holds `resume` ONLY when
3496
+ the engine would accept a resume (`resumePlanFor`: the same `readResume`/`CHECKPOINT_OUTCOMES`/
3497
+ `checkpointAgeGate` readers the engine refuses with, plus a signed `resolved-spec.json` /
3498
+ `resolved-spec-r<k>.json` beside the run whose hash is the one the run's `job-start` carries);
3499
+ otherwise `next` says why Resume is not available. `resume` on the detail is
3500
+ `{budgetUsd, maxWallMin, spentUsd, spendComplete}` (the confirm box's prefill) or `null`. The
3501
+ result glyphs are unchanged. The reuse button is described under Reuse workflow, below.
3502
+ **Status words (click-through 2026-10-04)** → every run row, run detail and imported row carries `status`
3503
+ `{key, sign, word}` from ONE table (`src/panel/status.js`): `[▶] running · [·] waiting · [✓] passed · [✗] failed ·
3504
+ [✗] capped · [✗] stopped · [?] died`; `glyph` is that sign. Mapping: green/already-green/satisfied → passed; no
3505
+ job-end + runner alive → running, gone → died; cap-halt, wall-halt, and an escalation that is a money-halt or wall-halt
3506
+ → capped; `stopped` → stopped; every other outcome (plan-red, check-red, step-red, close-red, provider-red,
3507
+ step-stalled, other escalations, refusals) → failed. The detail also carries `startedAt` and `endedAt` (the last
3508
+ `job-end` timestamp, null while live or died). `GET /api/imports` rows and `GET /api/imports/:id` carry `runStatus`
3509
+ (passed, or null when the bundle shows no green) and `runReason` (`goal met`). The Ended block's lines for died and
3510
+ stopped runs are `no ending recorded` and `you pressed Stop` (each `— resume` when Resume is offered).
3511
+ **Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
3512
+ `checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
3513
+ (blank = the signed caps). `404` unknown run; `409` when `resumePlanFor` says the engine would
3514
+ 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
3515
+ used) — refused at $0 before any write or spawn — or the monthly $
3516
+ limit (same library check and text as Sign & run). If a cap changed, the new spec is written as
3517
+ `resolved-spec-r<k>.json` beside the original and its hash is approved; the signed spec the run
3518
+ started under is never overwritten, and a run resumed under `r<k>` is resumed again under the
3519
+ spec whose hash its `job-start` carries. The spawn is `setsid --wait systemd-inhibit node
3520
+ bin/bareloop.mjs run-u --spec <spec> --resume <runid> --approve <hash>` (array argv, detached,
3521
+ log `resume-<runid>-<ms>.log` beside the spec; `--draft-spent-usd`/`--draft-spend-incomplete`
3522
+ ride along from the run's `job-start`, so the one cap still covers drafting). `--wait` hands back
3523
+ the engine's own exit code: an exit inside the settle window (6s) is the engine's own refusal and
3524
+ the log tail is returned as `error` verbatim; otherwise `{ok:true, runid, specHash, capsChanged,
3525
+ log}`. The run keeps its runid and its one list row: the resumed leg appends to the same spine (P5-R,
3526
+ `leg-start` in the run list), it never appears as a new run. The page's Resume button opens the
3527
+ caps form (prefilled, "spent so far") and `Sign & resume` is the human click.
3528
+ **Stop (item 5)** → `POST /api/runs/:runid/stop` (`src/panel/runroutes.js`, `checkHumanGuard`, no body).
3529
+ It writes the run's STOP REQUEST, a file `<spine>.stop` (`stopFilePath(spine)`, `src/legs.js` — the one
3530
+ spelling), and signals nothing. `404` unknown run; `409` when the run's LATEST leg is not live
3531
+ (`runIsAlive`) or its spine does not exist yet (still starting). The ENGINE half is library code, so
3532
+ CLI runs honour the file too (no new CLI command): `runJob`/`runPlan` take `stopFile`, and `run-u`/the
3533
+ panel's spawn pass `<spine>.stop`. The seam is the ROUND BOUNDARY of a step worker — where the money cap binds
3534
+ (`src/planrun.js`, the `metered` round callback): file present = delete it, emit `stop-requested
3535
+ {phase, step, iteration, round}`, end the Loop after that round; `ask` then throws category `stopped`
3536
+ and the step loop files it as job-end outcome `stopped` the way a mid-step cap-halt is filed
3537
+ (`spendComplete` exact — nothing is in flight at a round boundary). One owner, one check: a one-step run
3538
+ 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
3539
+ leg ends (`finally`), and at the start of the next leg (a click that raced a leg's end never stops a
3540
+ resume). `stopped` is in `CHECKPOINT_OUTCOMES`: resumable like a `cap-halt`, the same run continues as a
3541
+ new leg at the next step. `GET /api/runs/:runid` carries `live` and `stopping` (a stop request is on
3542
+ disk and the leg is live); Ended row `stopped` = "You stopped it." / "Resume." / `[Resume]`, card line
3543
+ `stopped — resume`, Audit divider `stopped: you stopped it · resumed <when>`. The page shows `[Stop]`
3544
+ in the Run tab's action area while live, "stopping after this turn…" after the click, and the same
3545
+ `[Resume]` there once the engine would accept one. The seam reads the file in STEP workers only
3546
+ (`phase` `step:<id>`): the scout, plan and close-fix rounds never read it, so a Stop clicked while the run
3547
+ is in one of those phases is not honoured until a step round runs again, and is otherwise cleared when
3548
+ the leg ends — the money and time caps still bind throughout.
3549
+ **Reuse workflow (replaces P5 item 3's Start from this, 2026-10-03)** — a button on GREEN runs only (the Run tab's
3550
+ action row and the Ended block; `ended.actions` carries `{id:'reuse', label:'Reuse workflow'}` on green and
3551
+ 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
3552
+ stopped/capped/died rows offer `resume` only) and on every imported job. It opens the Chat tab's New job card
3553
+ filled from the SIGNED job: a NEW run (new runid), never a rerun, nothing drafted ($0). **Only five boxes are
3554
+ open — Source, Destination, Model, $ cap, Time cap**; every other box (check type, model, job name, goal, success,
3555
+ guardrails, judge examples) is the signed workflow, greyed, and the SERVER refuses a start that changed one
3556
+ (`400 "<Field> is locked on a reused workflow — Clear the card to change it"`; the page is never trusted).
3557
+ Changing a locked box is Clear (the Chat card's top-right button), which drafts. Routes, behind `checkHumanGuard`:
3558
+ `GET /api/author/start-from?runid=<id>` or `?import=<id>` → `{ok, origin, card, from:'signed job'|'imported job',
3559
+ note, locked:[…], open:['source','destination','model','capUsd','maxWallMs'], specHash, workflowKey, trackRecord:{runs,
3560
+ green, notGreen, live, finished, avgSpendUsd, avgWallMs}, line}` (`404` unknown, `409` with an `error` when
3561
+ there is nothing to copy: no log, the run's signed job is not on disk, or an imported job that changed, lost its
3562
+ dependency or no longer matches its signed bytes); `POST /api/author/start` accepts `startFrom: <runid>` or
3563
+ `startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}`. The old
3564
+ `POST /api/author/start-from-check` and the "Changed — new job" mode are gone. **Identity, two keys:** the
3565
+ signature hash `jobSpecHash` is unchanged (it covers the caps and the write fence — what the person signs);
3566
+ `workflowKey(spec)` (`src/job.js`, not exported from the package root) is a sha256 over the signed spec WITHOUT
3567
+ `source`, `destination`, `writeScope`, `budgetUsd` and `maxWallMs` (`REUSE_OPEN_SPEC_FIELDS`) AND without `provider`,
3568
+ `baseUrl` and `model` (`WORKFLOW_KEY_IDENTITY_FIELDS`; `jobSpecHash` still covers all of them). A reuse copies the
3569
+ origin's signed spec, sets only `writeScope` (from Destination), `budgetUsd`, `maxWallMs` and the worker from the
3570
+ chosen Model Name (`buildReuseSpec(origin, card, rows)`: `provider`; `baseUrl` when the row has one; `model` only
3571
+ when the Name is not the provider's default tier — spelled as authoring spells it; a RUBRIC origin with no `judge`
3572
+ first gets `judge` = its own resolved judge identity so the judge never follows the worker; an explicit judge is
3573
+ kept; deterministic jobs get none), and signs under a
3574
+ NEW `jobSpecHash` with the SAME `workflowKey` (the session refuses a spec whose key moved). Locked boxes carry the
3575
+ signed values read by the Job tab's own readers. **Estimate line** above the card: "Same job — G green · N not
3576
+ 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 =
3577
+ one entry via `legsOf`; the averages read FINISHED runs with exact figures — spend from `chainSpend`, working time
3578
+ from `legsWallMs`, resume gaps excluded); unknown is said ("no finished run yet to price or time", "cost not
3579
+ 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
3580
+ re-verified at the moment of reuse — `readBundle` (the bundle hash covers every close script), `checkBundleDeps`,
3581
+ and every close stage's signed sha256 against the bytes on disk over the spec with `$BARELOOP_BUNDLE` resolved to
3582
+ the folder — then reused the same way; the close scripts stay in the verified folder (the engine re-verifies
3583
+ their bytes at run start and before every close run). An operator-written command close has no declaration for
3584
+ `prepareSigning` to ground (it refuses one by design), so that session's gate is the byte check, and the hash the
3585
+ 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
3586
+ prepared hash as soon as the session reports `prepared`; the server re-checks it). `card.json` (the form text)
3587
+ is still written beside `resolved-spec.json` when a session reaches `prepared`.
3588
+ **The progress list (2026-10-04)** — `GET /api/author/:id` state carries `steps: [{id, label, status:
3589
+ 'running'|'done'|'failed', detail}]`, one entry per pipeline step in first-seen order, updated in place (ids/labels:
3590
+ `STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A refusal sets
3591
+ `state.error` and fails exactly one step with the same text as its `detail`; pipeline text is never a chat message
3592
+ (`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
3593
+ fence (relative `writeScope` globs, never proven as a directory); for a folder source an absolute directory. Both
3594
+ are refused at $0, before any model call (the same rule on a fresh and a Reuse card): an ABSOLUTE
3595
+ Destination on a repo source → `Destination must be a path inside the repo, like src/digest.js` (before the
3596
+ copy); a source that is not a git repo → a plain refusal worded by the kind the source door recorded (a single
3597
+ file: "Source must be the repo folder — put the file in Destination"; a plain folder or a URL: "not a code
3598
+ project (no git repo found)").
3599
+ **Import, read only (item 4)** — `src/panel/importroutes.js`. The Workflows toolbar's `[Import]` opens a folder
3600
+ browser plus a paste box; an imported job is an exported bundle folder the person can LOOK at: nothing runs,
3601
+ nothing is signed, the folder is never written. Routes, all behind `checkHumanGuard` (token + own address —
3602
+ new disk exposure, so the strict guard, not the Host guard the plain GETs use): `GET /api/fs/list?path=`
3603
+ → `{ok, requested, path, parent, bundle, entries:[{name, path, bundle:true}], truncated, stopped, visited, depth}`
3604
+ (job BUNDLES only — a folder holding a regular file `manifest.json` — found by searching DOWN from `path` at most
3605
+ `depth` = 3 folders; `name` is the path relative to `path`, `path` the absolute one; symlinks never followed,
3606
+ `node_modules`/`.git`/dot-folders skipped, a bundle is never descended into, unreadable folders skipped silently;
3607
+ capped at 5000 folders visited (`stopped`) and 200 bundles (`truncated`); starts at the home folder when `path` is blank);
3608
+ `POST /api/imports {path}` → `{ok, id, job, dir}` (`readBundle` must come back clean — a plain or tampered
3609
+ folder is `400` and records nothing); `GET /api/imports` (the list, every bundle RE-READ now) and
3610
+ `GET /api/imports/:id` (one view). Imported list: `<home>/imports.jsonl`, one `{at, dir, job, bundleHash}` line
3611
+ 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
3612
+ sha256(real path). **Path safety:** the path is expanded (`~`, `~/…` only), refused if it holds a NUL byte, is
3613
+ over 4096 bytes or is not absolute, then `resolve`d and `realpath`ed — the REAL folder is listed and recorded
3614
+ and the response says which it chose (`path` beside `requested`); a non-directory is refused; a SYMLINK inside the
3615
+ search is never listed and never followed; names only, never file contents. **Status, re-read on every list/view:** `ok`, `changed` ("changed since import",
3616
+ red: a different/tampered/unreadable bundle) or `missing` ("folder not found"). The view carries goal,
3617
+ `checkType`, `success` (the close stage names), `guardrails`, `budgetUsd`/`maxWallMs`, `model`, the history
3618
+ the bundle shipped (`history:{greens,reds,total,recent}`, read from the bridges' history rows) and
3619
+ `approved`/`approvedText` — approval on THIS machine is `verifyBlessing` (the first green run here writes
3620
+ `blessing.json`): approved / not approved yet / stale. The row reads `imported · view only`; the view opens in
3621
+ the same Run / Audit / Job tabs as any run (Audit: "no log — this job ran on another machine"), and its one
3622
+ button, `[Reuse workflow]` in the Run tab's top action row, reuses the bundle's signed job as described above.
3623
+ `GET /api/imports/:id` also carries `runsHere` (how many `runs/<runid>/` folders in the bundle hold a readable
3624
+ `spine.jsonl`) and `run`, what the Run tab shows below the imported facts: `{kind:'spine', id, runid, at}` (the
3625
+ newest run in the bundle's own `runs/` that ended green; `id` is `<importId>~<runid>`), else `{kind:'bridge', runid,
3626
+ at, detail}` (the bridge's green version rendered as a run detail, fields the bridge does not carry left `null`),
3627
+ else `{kind:'none'}`; never throws. **An imported bundle's run is served read only** through the ordinary run routes
3628
+ under that composite id: `GET /api/runs/<importId>~<runid>` (detail), `…/audit` and `…/rounds` (and `…/job`) read
3629
+ the spine inside the bundle (`importrun.js` `safeSpinePath`: a checked name, `lstat`ed, a link never followed); an
3630
+ unknown import or run is `404`. Such a run is never in `GET /api/runs`, never live (`live`/`stopping` are false,
3631
+ no pid), never resumable or stoppable from the panel.
3632
+ **Run-card fixes (item 6)** → a listed run whose runner is alive but whose spine is not written
3633
+ yet reads `starting: true` (list AND detail; glyph `▶`, every figure null, `fileMissing:false`) —
3634
+ never `file missing`; a row whose runner is gone and whose spine is absent stays `fileMissing`.
3635
+ `GET /api/runs` rows now carry `spendFloorUsd`/`wallFloorMs` for a LIVE spine too (previously died
3636
+ only; both `null` once a `job-end` exists), the same `deriveDeath` floors the detail carries, so a
3637
+ live card reads "$X so far · running Ym" exactly like the right pane.
3638
+
3415
3639
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3416
3640
  (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
3417
3641
  U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
@@ -3509,7 +3733,8 @@ and leaves the worktree in place until the human removes it.
3509
3733
 
3510
3734
  **`history.jsonl` row shape** — one JSON object per line, one line per `bareloop run` on this
3511
3735
  machine: `{ runid, at, outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree,
3512
- branch, bundleHash, approveHash }`. `bundleHash` is the bundle's own signed manifest hash
3736
+ branch, bundleHash, approveHash, leg? }` (`leg` only from a resumed run's second leg: the same `runid`,
3737
+ one row per leg). `bundleHash` is the bundle's own signed manifest hash
3513
3738
  (unchanged across tightened runs); `approveHash` is the hash of the actual, possibly-
3514
3739
  tightened, `$BARELOOP_BUNDLE`-resolved spec that run executed — the two are recorded side
3515
3740
  by side so a tightened run's real approval can never be confused with the bundle's headline
@@ -3585,6 +3810,15 @@ spends; the CLI prints it to stderr, exit 2) and at the panel's Sign & run (`sig
3585
3810
  same text under `#jf-cap-money` via `GET /api/author/monthly-check?cap=`. "This month" is the local
3586
3811
  calendar month; a died or incomplete-spend run makes the total an "at least" figure.
3587
3812
 
3813
+ *Drafting spend that never became a run* (2026-10-05, F213). A panel authoring session writes
3814
+ `<session dir>/draft-spend.json` (`{ sessionId, spentUsd, spendComplete, provider, baseUrl, model,
3815
+ startedAt, updatedAt }`, atomic) after every metered call, so an abandoned, refused or restarted session's
3816
+ drafting money is not lost. `readLegs` (hence `monthSpend`, `spendSummary`, `checkMonthlyRoom` and the Money
3817
+ tab) adds every `<home>/panel-sessions/<id>/` that has one and whose folder holds no listed run's `spine` or
3818
+ `patient` (a signed run already carries its drafting spend via `--draft-spent-usd`; no double count). It lands in
3819
+ the month of `startedAt` (the first call), under the provider row it was drafted on; `spendComplete: false` or an
3820
+ unreadable file makes the figure an "at least". It is not a run: no wall, not in the `runs` count.
3821
+
3588
3822
  *Hold until done.* A run CLAIMS the limit when it starts and holds it until it is done — no lock file,
3589
3823
  records only. `claimRun` (`src/monthly.js`) writes the run's row FIRST (it carries its `pid` and
3590
3824
  `capUsd`), then reads `runs.jsonl`: earlier claims come first, and only the claims ABOVE its own row count
@@ -3622,12 +3856,16 @@ placeholder `no-key-needed`, never the word `null`). Each row: the env NAME (rea
3622
3856
  **Name** (the model id Chat uses), **API shape** (Anthropic / OpenAI-compatible / Gemini), **Base
3623
3857
  URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display only), and Balance
3624
3858
  (DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
3625
- `keys.<ENV NAME> = { name, shape, baseUrl }`. Defaults for a key with no saved entry:
3859
+ `keys.<ENV NAME> = { name, shape, baseUrl, priceInPerM?, priceOutPerM? }`. The last two columns are the customer's own price
3860
+ (**In $/1M**, **Out $/1M**, plain boxes; blank = not set, a number >= 0 otherwise; the server refuses a half-set pair with a plain
3861
+ error and saves nothing, and the page leaves a half-typed pair out of the save until both boxes are filled or both cleared).
3862
+ **Open keys folder** (`POST /api/settings/open-keys-folder`) opens `~/.config/bareloop` in the file manager (`xdg-open` / `open`,
3863
+ argv array, never the file itself); where it cannot, the page shows the path as text. Defaults for a key with no saved entry:
3626
3864
  `ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
3627
3865
  OpenAI-compatible / `https://api.deepseek.com/v1`; `OPENAI_API_KEY` OpenAI-compatible;
3628
3866
  `GEMINI_API_KEY` Gemini; `LOCAL_API_KEY` OpenAI-compatible / `http://127.0.0.1:11434/v1` (Ollama, LM
3629
- Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **No
3630
- price is shown anywhere** (display only — cost recording, the estimated provenance, spend sums, caps
3867
+ Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **The
3868
+ only price shown is the customer's own, typed in the two price boxes** (cost recording, the estimated provenance, spend sums, caps
3631
3869
  and the monthly limit are unchanged; a local model has no known rate, so its rounds come out as a
3632
3870
  non-zero loud guess, never a real $0).
3633
3871
 
@@ -3667,6 +3905,23 @@ const prepared = await prepareSource({
3667
3905
  Or the CLI: `node scripts/prep-source.mjs --source <path-or-url> --into <dir> [--destination
3668
3906
  <absolute-directory>]` — prints the tree path, the seed, and the exact next command.
3669
3907
 
3908
+ **A REPO source on a worktree (P6 item 1, the panel's repo jobs).** Pass `worktree: '<repo>/.bareloop/wt/<id>'` to
3909
+ `prepareSource` and a repo source is NOT copied: its tree is a detached `git worktree` of the person's own repo at their current
3910
+ commit (`src/worktree.js`, the one spelling the bundle door shares). `prepared.tree` is that worktree; `into` holds only
3911
+ `source.json`, which records `worktree`, `repo` and `seed` (= the repo's HEAD — no seed commit is made, no `output/` scaffold
3912
+ is written). Uncommitted edits in the person's repo are not in it (the panel says so at drafting start: "N uncommitted
3913
+ change(s) in your repo are not in this job — it starts from commit <sha>"). `.bareloop/` and `node_modules/` (any depth, so packages installed in the worktree never read as worker writes) are hidden from the person's own
3914
+ `git status` through the repo's private `<common-git-dir>/info/exclude`, never their tracked `.gitignore`. The person's git hooks never run when bareloop makes the worktree (either door). The signed run
3915
+ (`run-u --spec`) reads `worktree` from that manifest; its spine and books stay in the session directory beside `source.json`,
3916
+ never inside the person's repo. At the end: GREEN (an already-green run too, which has nothing to commit unless it is a resumed
3917
+ leg) ALWAYS commits the run's work on its `bareloop-<job>` work branch (authored by `bareloop`, hooks off, arbiter books and
3918
+ `node_modules` left out) — including a green that opened a review door or whose spine leaked — and removes the worktree folder,
3919
+ except while a review door is open: that folder is KEPT (a door accept re-runs the mechanical stages against it). The branch stays and the
3920
+ merge stays the person's (`git merge <branch>` / `git branch -D <branch>` are shown as text). Any other ending (stopped,
3921
+ capped, died, red) leaves the folder so Resume can re-enter it; a drafting session that ends without ever running (refused,
3922
+ abandoned) removes its worktree. A panel restart mid-drafting loses the in-memory session and leaves its worktree behind
3923
+ (`git worktree list` shows it; `git worktree remove --force <folder>` clears it).
3924
+
3670
3925
  > **If your source is a folder, read this first (hamr's ruling):** make a new folder, put
3671
3926
  > only the file(s) this job needs in it, point bareloop at that — never your original
3672
3927
  > folder. Everything in the folder you name is frozen, read, and paid for. `prep-source`
@@ -3768,13 +4023,14 @@ the run's own scratch tree.
3768
4023
  **`prepareSource` copies ONLY git-tracked files** (hamr's ruling, unchanged) — a JS/TS repo's
3769
4024
  prepared copy therefore never carries `node_modules`, and every close stage that needs a tool
3770
4025
  (`tsc`, a test runner) would instrument-stop with nothing telling the person why. bareloop
3771
- NEVER runs an install itself — it only ever touches gated primitives, never a shell-out to
3772
- `npm`/`pnpm`/`yarn`/`bun` on the person's behalf. `missingDependencies` is exported ($0,
4026
+ NEVER runs an install itself for the agent or the CLI doors — the agent only touches gated primitives, never a shell-out to
4027
+ `npm`/`pnpm`/`yarn`/`bun` on the person's behalf (the one exception is the panel door's own pre-token `npm ci --ignore-scripts` for an
4028
+ npm lock file, P6 item 3, `src/npminstall.js`; it is never a verb the agent holds). `missingDependencies` is exported ($0,
3773
4029
  JS/TS only for now — other languages return `null`, M3b's job): it takes `(treeDir,
3774
4030
  sourceSubdir?)` and walks from `sourceSubdir` up to `treeDir` (the same nearest-manifest rule
3775
4031
  `detectLanguage` uses, done independently to avoid a `source.js` ⇄ `detectlang.js` import
3776
4032
  cycle), and when the nearest `package.json` names a non-empty `dependencies`/`devDependencies`
3777
- with no sibling `node_modules`, it returns `{manager, command, reason}` — the exact install
4033
+ with no sibling `node_modules`, it returns `{dir, lockFile, manager, command, reason}` (`dir` = the folder holding the manifest, `lockFile` = the committed lock file or `null`) — the exact install
3778
4034
  line (`npm ci`/`pnpm install --frozen-lockfile`/`yarn install --frozen-lockfile`/`bun install
3779
4035
  --frozen-lockfile` by lockfile, else `npm install`), `cd`-prefixed for a subfolder job. An
3780
4036
  invalid `package.json`, no manifest found, or `node_modules` already present all return `null`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.34.0",
3
+ "version": "0.36.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/authorrun.js CHANGED
@@ -104,7 +104,7 @@ import { keysForDoor } from './keysfile.js';
104
104
  import { applyConfiguredKey, judgeRatesFor, keyNameFor, ratesFor, rowsForHome } from './providerrows.js';
105
105
  import { ConfigError } from './config.js';
106
106
  import {
107
- readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, statSync,
107
+ readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, statSync,
108
108
  } from 'node:fs';
109
109
  import { dirname, join, resolve } from 'node:path';
110
110
  import { createInterface } from 'node:readline';
@@ -120,6 +120,7 @@ import { defaultJudgeLoop, resolveJobJudge } from './judged.js';
120
120
  import { validateJob, jobSpecHash, resolveWorkerModel } from './job.js';
121
121
  import { scanSecrets, redactSecrets } from './validate.js';
122
122
  import { detectLanguage } from './detectlang.js';
123
+ import { moveFile } from './movefile.js';
123
124
  import { closeJudges, GATE_AUDIT_FILE } from './kinds.js';
124
125
  import { resolveProvider, buildRunnerProviders, apiKeyProblem } from './providers.js';
125
126
  import { commandFor } from './invoke.js';
@@ -288,7 +289,7 @@ export async function main(argv, deps = {}) {
288
289
  if (!existsSync(treeAudit)) return;
289
290
  const archived = join(OUT, `author-${runid}-${GATE_AUDIT_FILE}`);
290
291
  try {
291
- renameSync(treeAudit, archived);
292
+ moveFile(treeAudit, archived);
292
293
  out(`gate audit ${archived} (moved out of the patient tree — F186)`);
293
294
  } catch (e) {
294
295
  err(`gate audit could not be archived out of the patient tree: ${/** @type {NodeJS.ErrnoException} */ (e)?.message ?? e}`);