bareloop 0.31.0 → 0.33.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 (54) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +4 -1
  3. package/bareloop.context.md +232 -85
  4. package/bin/bareloop.mjs +0 -1
  5. package/package.json +1 -1
  6. package/src/authorflow.js +5 -3
  7. package/src/authorjob.js +3 -1
  8. package/src/authorrun.js +49 -11
  9. package/src/authorscout.js +10 -6
  10. package/src/bundlerun.js +266 -0
  11. package/src/cli.js +35 -357
  12. package/src/config.js +135 -0
  13. package/src/interviewrun.js +13 -4
  14. package/src/invoke.js +22 -0
  15. package/src/judged.js +5 -3
  16. package/src/keysfile.js +156 -0
  17. package/src/ledger.js +26 -0
  18. package/src/monthly.js +422 -0
  19. package/src/panel/authorroutes.js +61 -16
  20. package/src/panel/authorsession.js +46 -28
  21. package/src/panel/index.html +287 -13
  22. package/src/panel/server.js +58 -51
  23. package/src/panel/settingsroutes.js +230 -0
  24. package/src/planrun.js +7 -3
  25. package/src/providerrows.js +301 -0
  26. package/src/providers.js +3 -1
  27. package/src/run.js +7 -2
  28. package/src/runlist.js +92 -7
  29. package/src/source.js +8 -3
  30. package/src/u-watchdog.mjs +296 -0
  31. package/src/userrun.js +230 -54
  32. package/types/authorflow.d.ts +8 -2
  33. package/types/authorjob.d.ts +6 -1
  34. package/types/authorrun.d.ts +5 -0
  35. package/types/authorscout.d.ts +11 -2
  36. package/types/bundlerun.d.ts +26 -0
  37. package/types/cli.d.ts +2 -1
  38. package/types/config.d.ts +48 -0
  39. package/types/interviewrun.d.ts +5 -0
  40. package/types/invoke.d.ts +8 -0
  41. package/types/judged.d.ts +5 -1
  42. package/types/keysfile.d.ts +90 -0
  43. package/types/ledger.d.ts +14 -0
  44. package/types/monthly.d.ts +227 -0
  45. package/types/panel/authorroutes.d.ts +4 -2
  46. package/types/panel/authorsession.d.ts +7 -15
  47. package/types/panel/server.d.ts +10 -3
  48. package/types/panel/settingsroutes.d.ts +14 -0
  49. package/types/planrun.d.ts +13 -1
  50. package/types/providerrows.d.ts +229 -0
  51. package/types/run.d.ts +14 -1
  52. package/types/runlist.d.ts +66 -3
  53. package/types/source.d.ts +8 -3
  54. package/types/userrun.d.ts +54 -0
@@ -91,8 +91,9 @@ inherited rule carries the green that minted it and the contrast that attributed
91
91
  chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin — no grading path reads it.
92
92
  The judge's key follows the resolved judge provider's own `envKey`, with `JUDGE_API_KEY`
93
93
  as a role-named override in front of it. The bundle runner
94
- (`bareloop run`) is `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would
95
- have needed, rather than constructing another provider with the wrong key.
94
+ (`bareloop run`) is a door to this same engine (`src/bundlerun.js` → `src/userrun.js`), so it
95
+ reads whatever key the bundle's provider names (keys file or env) and refuses a missing one
96
+ at $0, exit 2, naming it.
96
97
  - **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
97
98
  `bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
98
99
  ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
@@ -1160,7 +1161,7 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
1160
1161
  `coordination-red` — a failure between units (scope contention, step order, store
1161
1162
  races), never to be folded into worker/interpreter reds.
1162
1163
 
1163
- ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, draftSpentUsd?, draftSpendComplete?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
1164
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, rates?, judgeRates?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, draftSpentUsd?, draftSpendComplete?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
1164
1165
 
1165
1166
  **`draftSpentUsd` (hamr's ruling 2026-09-28, "one cap covers drafting + run") — money the
1166
1167
  AUTHORING pipeline already spent on this job before it was signed.** Shrinks THIS run's own
@@ -1215,9 +1216,9 @@ readout seed (grades) — the distinction matters and is spelled out there.
1215
1216
 
1216
1217
  `resumable` (default `true`, PRD item 27(c)/F130) says whether THIS runner supports
1217
1218
  `--resume` at all — `run-u.mjs` leaves it at the default (byte-identical to before this
1218
- flag existed); the exported bundle CLI (`src/cli.js`) passes `false` so the run's
1219
- escalation tail says "resume is `run-u`-only in v1" instead of naming a flag it does not
1220
- implement.
1219
+ flag existed). The exported bundle CLI is no longer a second `runJob` caller (`bareloop run`
1220
+ goes through `src/userrun.js`), so it leaves it at the default too and supports
1221
+ `bareloop run <bundle> --resume <runid>`; the F130 `resumable: false` was retired with `doRun`.
1221
1222
 
1222
1223
  The runner — the shell's top layer, and the ONE entry. It composes everything below it and
1223
1224
  interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
@@ -1306,6 +1307,11 @@ knob, not a product default — the spec names no scout, so the signed hash is u
1306
1307
  `--read-shim`: an unrecognised value exits 2 at argv, and every re-invocation the runner prints
1307
1308
  carries `--scout off` when set, so a resume never silently drops the arm.
1308
1309
 
1310
+ Every re-invocation the runner prints (the preview's approve line, resume, pause and reopen hints)
1311
+ names the key variable the run really reads (the job's key row, not the provider shape's default
1312
+ name) and carries `--registry <dir> --workflow <name>` whenever they were given, so a pasted command
1313
+ never greens with no registry row (F207).
1314
+
1309
1315
  **`bareloop run-u --draft-spend-incomplete`** is a boolean presence-flag (no value) that sets
1310
1316
  `runJob`'s `draftSpendComplete: false`, saying the `--draft-spent-usd` figure is a floor. It
1311
1317
  has no guard against being passed without `--draft-spent-usd` — alone it is inert (nothing is
@@ -1843,19 +1849,24 @@ bare-agent's `Loop` takes `rates: {in, out, cacheReadMult?, cacheWriteMult?}`
1843
1849
  tokens, the two multipliers applying to the input rate for the cache-read and cache-write
1844
1850
  tiers (defaulting to Anthropic's 0.1× / 1.25×). A caller-supplied rate is recorded as
1845
1851
  VOUCHED rather than as a guess, and it silences bare-agent's own guesstimate warning.
1846
- **Named, not papered over:** `runJob`/`runPlan` construct their own `Loop` and do not yet
1847
- accept a `rates` passthrough, so today that option is reachable only by a caller driving
1848
- bare-agent directly — a bareloop-run job is priced by the guesstimate, full stop. The
1849
- passthrough is an open follow-up, and until it lands every paragraph above describes the
1850
- only pricing a `runJob` adopter gets.
1851
-
1852
- **The provenance is on the record, per round (`rateSource`).** The field arrives with
1853
- bare-agent **>= 0.37**; under the pinned `^0.36.0` no provider payload carries it yet, so
1854
- every priced round this library writes today reads UNKNOWN provenance — correctly, and by
1855
- the same rule that governs the archive. (The one exception is the native per-turn
1856
- `worker-turn`, whose `null` is bareloop's OWN statement rather than a forwarded one: that
1857
- surface prices the SESSION, so a turn had no rate to guess.) Once the pin moves, every `worker-round` /
1858
- `worker-turn` the plan flow writes carries bare-agent's own label beside `pricing`:
1852
+ **Where bareloop takes it from:** the CUSTOMER's own price on the key's row in
1853
+ `~/.config/bareloop/config.json` (`keys.<ENV NAME>.priceInPerM` / `priceOutPerM`, USD per 1M
1854
+ tokens — see "Settings" below). `runJob` / `runPlan` accept `rates` (and `judgeRates`) as data and
1855
+ hand them to the one `Loop` that drives worker, scout and planner rounds; the run doors resolve
1856
+ them once. No price on the row = no `rates` key anywhere = the guesstimate above, byte-identical to
1857
+ before. bareloop keeps no price list of its own, ever.
1858
+
1859
+ **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.43.0`)
1860
+ carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
1861
+ `src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
1862
+ `pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
1863
+ (the guess). The exceptions read UNKNOWN provenance, correctly, and by the same rule that governs
1864
+ the archive: `judge-round` records carry no `rateSource` (the judge cost payload,
1865
+ `src/kinds.js` `onJudgeCost`, does not forward it, so a judge round is never counted as vouched,
1866
+ priced by you or not), and so does every round archived before the signal existed. The native
1867
+ per-turn `worker-turn` carries `null`, bareloop's OWN statement rather than a forwarded one: that
1868
+ surface prices the SESSION, so a turn had no rate to guess. A `worker-round`/`worker-turn`
1869
+ carries bare-agent's own label beside `pricing`:
1859
1870
  `'provider'` (the provider reported its own authoritative cost — the native CLI surface)
1860
1871
  and `'caller'` (you passed the rate) are VOUCHED; `'tier'` (a recognized Claude tier,
1861
1872
  where **both** of bareloop's own production models land) and `'default'` (the blind
@@ -2986,7 +2997,8 @@ separate tarball step).
2986
2997
  README.md the operator questions, in the order `bareloop run` asks them
2987
2998
  blessing.json absent at export; written by the run that first greens it
2988
2999
  history.jsonl absent at export; one line appended per `bareloop run` on this machine
2989
- runs/<runid>/ absent at export; one per run — that run's own spine.jsonl + gate-audit.jsonl
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?})
2990
3002
  ```
2991
3003
 
2992
3004
  **`bundleHash`** = sha256 over the sorted `path:contentSha256` lines of exactly `spec.json`
@@ -3001,7 +3013,7 @@ a `package.json` (name/version/`dependencies: {"bareloop": "^…"}`, `private: t
3001
3013
  corrects an earlier, now-stale reading of this section that claimed no `package.json` was
3002
3014
  written; `tests/bundle.test.js` proves both the write and the hash-exclusion directly. The
3003
3015
  `runs/<runid>/` directory (this run's relocated `spine.jsonl`/`gate-audit.jsonl`,
3004
- written by `bareloop run`, step 6 below) is real and live but is not in the frozen spec's
3016
+ written by `bareloop run`, step 6 below; `run.json` records where the worktree is, for a resume) is real and live but is not in the frozen spec's
3005
3017
  layout table at all. `history.jsonl`'s row also carries `bundleHash` and `approveHash`
3006
3018
  (the POC-fact correction, below) beyond the fields the original layout table named.
3007
3019
 
@@ -3126,8 +3138,8 @@ script leaves the signed spec's own hash intact. `runJob`'s close-first precheck
3126
3138
  one — a fake green, $0 spent, no work done, reported as success. `readBundle`'s
3127
3139
  `bundle-tampered` red is therefore not a courtesy check: it is the only thing standing
3128
3140
  between a swapped script and a fake green, so `bareloop run` calls it as literal step 1,
3129
- before the envelope check, before the key check, before the worktree, before `runJob` is
3130
- ever reached.
3141
+ before the envelope check, before the blessing check, before any key is read, before the
3142
+ worktree, before the engine is ever reached.
3131
3143
 
3132
3144
  #### The CLI — `bareloop export | run | history | replay | run-u | interview | author`, `bin/bareloop.mjs`
3133
3145
 
@@ -3138,7 +3150,7 @@ ever reached.
3138
3150
  (`export { main as cliMain } from './cli.js'` — renamed on export so it never collides with
3139
3151
  some other module's own generic `main`). `cliMain(argv, deps)` is the in-process test/
3140
3152
  integration seam: pass `deps.provider` (and optionally `providerFor`/`judgeProvider`) and the
3141
- real `ANTHROPIC_API_KEY` check is skipped entirely — a scripted provider *is* the run, the
3153
+ real key check is skipped entirely — a scripted provider *is* the run, the
3142
3154
  same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
3143
3155
  `now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
3144
3156
 
@@ -3148,7 +3160,9 @@ below. `run-u`, `interview` and `author` (below) are not on this menu (PANEL-BUI
3148
3160
  tasks 2/4-4/4): each one's own flag grammar is too wide for a line-at-a-time wizard, and the
3149
3161
  menu's job is to ask the SAME questions the sub-command below already answers, never to
3150
3162
  invent a new interview — this is doubly true for `interview`/`author`, which already ARE
3151
- interviews of their own. All three are dispatched by name only: `bareloop run-u <flags…>`,
3163
+ interviews of their own. Choice `2 run` is handed the SAME resolved keys home `bareloop run <bundle>` gets, so the
3164
+ monthly limit, your price and the key-row choice apply exactly as on the sub-command (before this it
3165
+ got none, and a run started from the menu skipped the limit). All three are dispatched by name only: `bareloop run-u <flags…>`,
3152
3166
  `bareloop interview <flags…>`, `bareloop author <flags…>`.
3153
3167
 
3154
3168
  - **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
@@ -3158,54 +3172,63 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3158
3172
  missing flag, `0` on success.
3159
3173
 
3160
3174
  - **`bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]`**
3161
- — the exact order below; no step is ever reordered:
3175
+ and **`bareloop run <bundleDir> --resume <runid> [--repo <path>] [...]`** — a thin door
3176
+ (`src/bundlerun.js`) to the same engine `bareloop run-u` drives (`src/userrun.js`): one
3177
+ runner, so providers, keys (any provider the bundle's spec names — the keys file or env), the
3178
+ judge, the monthly $ limit, the outside watchdog and the readout are that engine's. The door
3179
+ does only what is bundle-specific, in this order, no step reordered:
3162
3180
  1. `readBundle(bundleDir)` — the tamper check (N4, above). Any red stops here, exit `1`.
3163
3181
  2. `checkBundleDeps(bundleDir)` (F128) — can the bundle's own `close/` directory resolve
3164
- `require.resolve('bareloop')`? A bundle installed the WRONG way (as someone else's
3165
- dependency — see the corrected adopter flow below) has no `node_modules` of its own,
3166
- which would otherwise crash the close deep inside the precheck with a bare
3167
- `ERR_MODULE_NOT_FOUND`. A red here (`bundle-deps-missing`) prints the exact cure line
3182
+ `require.resolve('bareloop')`? A red (`bundle-deps-missing`) prints the exact cure line
3168
3183
  (`cd <bundleDir> && npm install`) and stops, exit `1`, before the envelope check, the
3169
- key, or any worktree.
3184
+ blessing check, any key, or any worktree.
3170
3185
  3. `checkEnvelope(spec, { budgetUsd?, maxWallMs? })` — `--wall` is minutes, converted to ms.
3171
3186
  A red (an invalid number, or a widen) stops here, exit `1`.
3172
- 4. The provider key: `deps.provider` if the caller injected one (the test seam), else
3173
- `ANTHROPIC_API_KEY` from `env`. Absent and no injected provider: print the bundle's own
3174
- `README.md` plus `bundleHash:`, spend nothing, **exit `0`** (this is a legitimate,
3175
- non-error stop, not a red).
3176
- 5. Blessing. No `blessing.json` yet: print the "first run" notice + the minting-run line
3177
- (F128-corrected — see below), the `bundleHash`, and **require** `--approve <bundleHash>`
3178
- to match exactly, or exit `1`. A `blessing.json` present: `verifyBlessing` must pass
3179
- (`blessing-stale` stops here, exit `1`); a `--approve` flag is accepted but ignored with
3180
- a printed note ("no-resign" — hamr's ruling).
3181
- 6. The worktree: refuse (exit `1`) if `--repo` is not a git repo with ≥1 commit, or if
3182
- `<repo>/.bareloop/wt/<runid>` already exists; otherwise `git worktree add --detach
3183
- <repo>/.bareloop/wt/<runid> HEAD` — **always fresh, never reused** (a reused worktree
3184
- would read a prior run's edits as "already-green" — a negative POC's exact finding).
3185
- 7. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/
3186
- `maxWallMs` on that resolved spec in memory if `--budget`/`--wall` were given → mint a
3187
- **fresh** `approveHash = jobSpecHash(that tightened+resolved spec)` (the runner signs
3188
- the spec it is actually about to run; the human-approved `bundleHash` never changes) →
3189
- `runJob(runSpec, { approvals: [{ specHash: approveHash, signer: 'bundle', ts }], workdir:
3190
- worktree, provider, providerFor, judgeProvider, emit, shellCapUsd: runSpec.budgetUsd,
3191
- readShim: 'cap', scout: true })`, spine written to `<bundleDir>/runs/<runid>/spine.jsonl`.
3192
- A thrown `runJob` crash is caught and reported, exit `1`.
3193
- 8. The worktree's `gate-audit.jsonl` is moved to `<bundleDir>/runs/<runid>/gate-audit.jsonl`
3194
- (same relocation `src/userrun.js`, lifted out of `scripts/run-u.mjs`, does for in-repo runs). `spentUsd`/`spendComplete`
3195
- are read off this run's own `job-end` spine event — never fabricated as `0` when unknown.
3196
- 9. `appendHistory` — one `history.jsonl` line: `{ runid, at, outcome, spentUsd,
3197
- spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash, approveHash }` (the
3198
- `bundleHash` ↔ `approveHash` pairing, POC fact 2, so the human-signed hash and the
3199
- hash actually enforced can never drift apart silently). A `green` outcome on a still-
3200
- unblessed bundle also calls `bless(bundleDir, { bundleHash, runid, outcome, host })`.
3201
- 10. The tail: `outcome`, `spent` (`≥$…` when `spendComplete === false`, `UNKNOWN` when
3202
- `spentUsd` is `null`), `branch`, `worktree`, a `git merge <branch>` instruction
3203
- ("merge stays human — this CLI never merges"), and "the worktree is kept until you
3204
- remove it: `git worktree remove <worktree>`" (F110: never delete it for the user).
3205
- **Exit `0` ONLY for `green`/`already-green` (F128) — every other outcome (close-red,
3206
- plan-red, escalated, cap/wall-halted, provider-red, a crashed `runJob`, …) exits `1`,**
3207
- so a caller scripting off the exit code can never mistake a red run for a green one.
3208
- The tail's printed lines are unchanged either way.
3187
+ 4. Blessing — BEFORE any key is read. No `blessing.json` yet: print the bundle's `README.md`
3188
+ (the operator questions), the "first run" notice + the minting-run line (F128-corrected —
3189
+ see below), the `bundleHash`, and **require** `--approve <bundleHash>` to match exactly, or
3190
+ exit `1`. A `blessing.json` present: `verifyBlessing` must pass (`blessing-stale` stops here,
3191
+ exit `1`); a `--approve` flag is accepted but ignored with a printed note ("no-resign").
3192
+ A resume passes the same checks (an unblessed bundle whose first run halted still needs
3193
+ `--approve`).
3194
+ 5. The tree, planned not created: a fresh run refuses (exit `1`) if `--repo` is not a git repo
3195
+ with ≥1 commit or `<repo>/.bareloop/wt/<runid>` already exists; the engine then creates
3196
+ `git worktree add --detach <repo>/.bareloop/wt/<runid> HEAD` only once ITS OWN $0 refusals
3197
+ (missing key, monthly limit, param guards) have passed, so a refusal leaves no worktree —
3198
+ **always fresh, never reused** (a reused worktree would read a prior run's edits as
3199
+ "already-green"). A **resume** (`--resume <runid>`) instead reads `runs/<runid>/run.json`,
3200
+ re-enters that run's own worktree (a missing `run.json`, a vanished worktree, or a `--repo`
3201
+ different from the recorded one is a stop, exit `1`, spending nothing; `--repo` is optional
3202
+ on a resume) and hands the engine `resumeRun` on that run's spine — the engine's own gates
3203
+ (checkpoint age, tree-at-seed, liveness) and spend fold apply, so `--budget` can never widen
3204
+ a resume. The new leg gets its own `runs/<newid>/`. The resume command a halted leg prints carries that leg's own
3205
+ `--budget`/`--wall`, only if you passed them (a tightened ceiling never silently reverts to the signed
3206
+ one). After a MONEY halt the line does NOT repeat the exhausted `--budget` (chain spend is folded, so
3207
+ 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. A bundle's spec is
3209
+ manifest-hashed, so its halt hint never asks you to edit `budgetUsd`.
3210
+ 6. `resolveBundleSpec` (the `$BARELOOP_BUNDLE` substitution) → tighten `budgetUsd`/`maxWallMs`
3211
+ in memory if `--budget`/`--wall` were given → `approveHash = jobSpecHash(that tightened+
3212
+ resolved spec)`, handed to the engine as the approval it checks against (`signer: 'bundle'`;
3213
+ the human-approved `bundleHash` never changes). The spine, the close's books, the relocated
3214
+ `gate-audit.jsonl` and `run.json` are written under `<bundleDir>/runs/<runid>/`.
3215
+ 7. Engine outcomes the door keeps: a missing key is the engine's refusal, **exit `2`**, naming
3216
+ the key (it used to print the README and exit `0`); the close-fix cap is run-u's `CAP_RUNS`
3217
+ (4 — one number for every caller; a bundle used 3 before); the monthly limit applies (exit
3218
+ `2`, nothing spent); a bundle green mints **no bridge file** (a bundle's bridges are shipped
3219
+ inputs); `--door`/`--decide`/`--review-door` are not exposed on `run`.
3220
+ 8. `appendHistory` — one `history.jsonl` line off this leg's own `job-end`: `{ runid, at,
3221
+ outcome, spentUsd, spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash,
3222
+ approveHash, resumedFrom? }` (the `bundleHash` ↔ `approveHash` pairing, POC fact 2).
3223
+ `spentUsd`/`spendComplete` are never fabricated as `0` when unknown. No `job-end` (a $0
3224
+ refusal, a crash) writes no row. A `green` outcome on a still-unblessed bundle also calls
3225
+ `bless(bundleDir, { bundleHash, runid, outcome, host })`.
3226
+ 9. The tail: the engine's readout (`outcome`, `spent` with `≥$…` when incomplete, …), then the
3227
+ door's `branch`, `worktree`, a `git merge <branch>` instruction ("merge stays human — this
3228
+ CLI never merges") and "the worktree is kept until you remove it: `git worktree remove
3229
+ <worktree>`" (F110). **Exit `0` ONLY for `green`/`already-green` — every other outcome
3230
+ exits `1`**, and the engine's `2` (refusal) / `3` (spine leak) are preserved, so a caller
3231
+ scripting off the exit code can never mistake a red run for a green one.
3209
3232
 
3210
3233
  **Adopter flow, corrected (F128, 2026-09-06 — hamr's paid fire found the frozen spec's
3211
3234
  validation step 2 named the wrong install shape):** `npm install <bundle dir>` run from a
@@ -3302,17 +3325,17 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3302
3325
  `baselineKind`/`question`: the plain-English question is derived in `src/panel/server.js`
3303
3326
  (`stageQuestionText`) from the signed stage's kind/params only, `null` when no honest
3304
3327
  wording exists (the client then shows the stage's own name). Every
3305
- endpoint is GET/HEAD only (anything else — including every write verb — is `405`); a URL
3328
+ P1 endpoint is GET/HEAD only (anything else — including every write verb — is `405`, outside the
3329
+ two human-click-guarded write families `/api/author/*` and `/api/settings/*` below); a URL
3306
3330
  never joins a path segment into a filesystem read — a runid is looked up in the run list
3307
3331
  first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
3308
3332
  suffix too, and every endpoint echoes back the LISTED runid, never a filename-derived one
3309
3333
  that can silently drop that suffix), and only the path THAT ROW stores is ever read. A
3310
3334
  taken port fails loudly (names the port, exit `1`) — this command never silently tries a
3311
- different one. No interview, no author, no run trigger, no key/`.env` ever read on this
3312
- path. One more caller of `src/replayio.js`/`src/runlist.js`, in-process, per the layering
3335
+ different one. This read path runs no interview, no author and no run, and reads no key
3336
+ value (the write families below do, server-side, and never return one). One more caller of `src/replayio.js`/`src/runlist.js`, in-process, per the layering
3313
3337
  law (`docs/product/PANEL-BUILD.md` §2) — the panel's HTTP handler holds no flow logic of
3314
- its own. Chat/Settings/authoring (P3/P4) are not built yet; the page says so rather than
3315
- hiding the gap.
3338
+ its own. Chat/authoring (P3) and Settings (P4a/P4b) are built — see below.
3316
3339
 
3317
3340
  - **Panel P2 (live run view, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → the page
3318
3341
  polls, it does not push (no SSE/websocket). One `setInterval` (2s) drives two throttled
@@ -3331,14 +3354,19 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3331
3354
  `resolveAuditPathForRow` (`src/panel/server.js`), the one shared owner the Run tab's tools
3332
3355
  summary and the Audit tab both read through — tried only while the run's own spine has no
3333
3356
  `job-end` yet, so a finished run never risks reading a stale leftover file at that path.
3334
- `scripts/replay-live.mjs <sourceSpine> <outDir> [--speed N]` is a dev-only, unshipped ($0,
3357
+ `scripts/replay-live.mjs <sourceSpine> <outDir> [--speed N] [--live-audit]` is a dev-only, unshipped ($0,
3335
3358
  no provider call) instrument that paces a real archived spine's records back out under a
3336
3359
  new runid so the panel can be exercised against a "live" run without a paid run — it is not
3337
3360
  a substitute for watching a real run start-to-end, which stays a separately authorized step.
3338
-
3339
- - **Panel P3 (chat/authoring, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → the ONE
3340
- family of write routes the panel serves, all under `/api/author/*` (`src/panel/
3341
- authorroutes.js`); every other route stays GET/HEAD-only per P1's own rule. Every route
3361
+ `--live-audit` (opt-in) puts the sidecar at a scratch patient dir during the replay so the
3362
+ live-fallback path above is exercised, then moves it to the sibling name at the end.
3363
+
3364
+ - **Panel P3 (chat/authoring, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → one of the
3365
+ panel's TWO families of write routes (the other is `/api/settings/*`, see "Settings" below), this
3366
+ one under `/api/author/*` (`src/panel/authorroutes.js`); every other route stays GET/HEAD-only
3367
+ per P1's own rule. A POST body over 1 MiB is refused `413` before it is parsed, and the job
3368
+ card's `jobsDir` (where an existing job name is looked up) is a server-side seam only, never read
3369
+ off the client's card. Every route
3342
3370
  requires the human-click guard: an `x-bareloop-token` header matching a fresh token minted
3343
3371
  once per server start (templated into `index.html` like the port) AND an Origin/Host
3344
3372
  naming this exact `127.0.0.1:<port>` — a request failing either gets `403`, before the
@@ -3368,14 +3396,14 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3368
3396
  [`--draft-spend-incomplete`] when the session's drafting spend is > 0 — one cap covers
3369
3397
  drafting + run) detached (array argv,
3370
3398
  never a shell string), with its own log file inside the session's own dir, and the
3371
- server's own environment (a key is never read into or sent to the page — a missing one
3372
- refuses the session at $0, naming only the env var). `src/panel/authorsession.js` takes
3399
+ server's environment with `~/.config/bareloop/.env` merged in per request (a key is never read
3400
+ into or sent to the page — a missing one refuses the session at $0, naming only the env var). `src/panel/authorsession.js` takes
3373
3401
  test-only DI seams (`scout`/`generate`/`confirmGenerate`/`authorFn`/`prepareSigningFn`) so
3374
3402
  a test can drive the real ask()-channel/revise/hash wiring without a live provider call;
3375
3403
  none of them are reachable from `authorroutes.js`'s real construction path. Sessions live
3376
3404
  under `~/.config/bareloop/panel-sessions/<id>/` (each one's own `resolved-spec.json` and
3377
3405
  `signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
3378
- `~/.config/bareloop/.env` keys-file loader, and Settings are P4, not built here.
3406
+ `~/.config/bareloop/.env` keys-file loader, and Settings have landed (P4a/P4b — see "Settings" below).
3379
3407
 
3380
3408
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3381
3409
  (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
@@ -3393,6 +3421,11 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3393
3421
  preview `scripts/run-u.mjs --job <x>` prints with no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
3394
3422
  set. `scripts/run-u.mjs` is unchanged in behaviour: it is the pre-existing thin adapter
3395
3423
  over this same `src/userrun.js:main`, not rewired to go through `src/cli.js`.
3424
+ **Printed commands are spelled the way you entered:** every re-invocation these flows print (the approve
3425
+ line, resume, pause, reopen, the interview's run-author line, the author's run-u line) reads `bareloop
3426
+ run-u …` / `bareloop author …` when reached through `bareloop`, and `node scripts/run-u.mjs …` only through
3427
+ the source-tree adapters — the tarball ships no `scripts/`. One owner: `commandFor` (`src/invoke.js`),
3428
+ fed by each flow's `deps.invokedAs`.
3396
3429
 
3397
3430
  - **`bareloop interview <flags…>`** (PANEL-BUILD.md P0 task 3/4) → the close-authoring
3398
3431
  interview, at the terminal, one question at a time (D10; `docs/logs/FINDINGS.md`'s
@@ -3490,6 +3523,119 @@ provider but `anthropic-api`, spine bundling for `formatReplay` on the minting r
3490
3523
  `runid` is noted), and tarball packing — a bundle is a plain directory; use `npm pack` on it
3491
3524
  yourself if you want a tarball.
3492
3525
 
3526
+ ### Settings — the keys file, `config.json`, the monthly $ limit, the Providers tab (PANEL-BUILD.md P4a, `src/keysfile.js`, `src/config.js`, `src/monthly.js`, `src/providerrows.js`, `src/panel/settingsroutes.js`)
3527
+
3528
+ Two files in `~/.config/bareloop/`, outside any repo:
3529
+
3530
+ - **`.env` — key VALUES only.** Plain `NAME=value` lines (optional `export `, `#` comments, one pair
3531
+ of quotes). `loadKeysEnv({ env?, home? })` merges it over the environment; **the shell wins** —
3532
+ the file only fills names the shell leaves unset. Mode other than 600 = a `warning` string
3533
+ (printed on CLI start, shown in Settings), never a refusal. Values go only to provider
3534
+ construction; the page and every log get NAMES and found / not set. `keysForDoor(deps)` is the
3535
+ door helper: an injected `env` with no `keysHome` skips the file entirely (tests never read the
3536
+ real one). The CLI (`run-u`, `interview`, `author`) and the panel both use it.
3537
+ - **`config.json` — every setting, never a key value.** `readConfig({ home })` /
3538
+ `updateConfig(patch, { home })` (atomic tmp + rename, mode 600, unknown fields kept).
3539
+ Fields: `monthlyLimitUsd` (number above 0; absent or `null` = no limit; any other value present
3540
+ is a `ConfigError`, refused like an unreadable file), `anthropicBalanceNote` (a note the
3541
+ person types; no check ever reads it), `keys.<ENV NAME>` = `{ name, shape, baseUrl, priceInPerM?, priceOutPerM? }` (the Providers rows' settings, plus the customer's own price below). A secret-shaped
3542
+ string anywhere in the document is refused on save. An unreadable file is a `problem` string and
3543
+ a refused run start ($0), never "no limit". There is no monthly TIME limit (dropped).
3544
+
3545
+ **Your own price — `keys.<ENV NAME>.priceInPerM` / `priceOutPerM`.** The customer sets the price of the
3546
+ model behind a key, on that key's row, in USD **per 1M tokens** (input and output), for example
3547
+ `"keys": { "DEEPSEEK_API_KEY": { "priceInPerM": 0.006, "priceOutPerM": 1.2 } }`. There is no built-in
3548
+ price list. Both numbers or neither: each must be a finite number 0 or more, and one alone, a negative,
3549
+ a string or `null` refuses the run at $0 (`ConfigError`, exit 2, "Nothing spent.") — a bad price never
3550
+ falls back to the guess. A Settings save keeps the two fields (the Settings SCREEN has no price field
3551
+ yet; edit `config.json`). `ratesFor(provider, baseUrl, rows, model)` (`src/providerrows.js`) is the one
3552
+ lookup, on the SAME row `keyNameFor` picks the key from (`findRow`: shape + endpoint + model), returning
3553
+ `null` (no price) or `{ rates: {in, out}, inPerM, outPerM, envName }` with `rates` in bare-agent's
3554
+ per-1K unit (`perM / 1000`). The run doors resolve it once, at $0, before the monthly claim and any
3555
+ worktree or spine (`src/userrun.js`; `src/authorrun.js`; the panel's authoring session): the worker row
3556
+ prices worker, scout and planner rounds; the judge is priced at ITS model's own row — for a different
3557
+ provider its own row, for the worker's provider the row on the worker's endpoint named for the judge's model
3558
+ (`judgeRatesFor`, `src/providerrows.js`), falling back to the worker's price when the model names no row.
3559
+ (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
+ no price it stays the built-in guess, as before. The run preview and the run tail print
3561
+ `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. **The price is yours, and it sets what the cap means:** the cap and the halts run
3563
+ on the dollars your price produces, so a price set too low weakens the cap (a run can do far more
3564
+ than you meant before it halts). When a vendor lists two prices (for example a cache-hit and a
3565
+ cache-miss input rate) enter the higher one. bare-agent applies its own cache multipliers (read 0.1×,
3566
+ write 1.25× of the input rate) to cache tokens; they are not configurable here, so cache tokens are
3567
+ priced from your input price by that rule. A price applies from the next run: **past runs are never
3568
+ re-priced** (a spine and the run list keep the dollars they booked). Not covered by a price: a native
3569
+ (CLI) session, which reports its own cost, and the reuse engine (`runReuse` / `selectBridge`), which
3570
+ resolves no key row.
3571
+
3572
+ **Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
3573
+ atLeast }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals> (monthly
3574
+ limit)`. It is called at the run-start seam in `src/userrun.js` `execute` (before any token
3575
+ spends; the CLI prints it to stderr, exit 2) and at the panel's Sign & run (`signRun` in
3576
+ `src/panel/authorroutes.js`, refused server-side whatever the page shows). The page only echoes the
3577
+ same text under `#jf-cap-money` via `GET /api/author/monthly-check?cap=`. "This month" is the local
3578
+ calendar month; a died or incomplete-spend run makes the total an "at least" figure.
3579
+
3580
+ *Hold until done.* A run CLAIMS the limit when it starts and holds it until it is done — no lock file,
3581
+ records only. `claimRun` (`src/monthly.js`) writes the run's row FIRST (it carries its `pid` and
3582
+ `capUsd`), then reads `runs.jsonl`: earlier claims come first, and only the claims ABOVE its own row count
3583
+ (rows below it yield to it). A claim above is held at the larger of its spend so far and its cap while
3584
+ its pid is a live bareloop runner (`isLiveRunner`, `src/runlist.js`, also what `--resume` uses to refuse a
3585
+ resume against a live run: the process is a node executable with a `bareloop`/`bareloop.mjs`/`run-u.mjs`/
3586
+ `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
3588
+ finds it, with a note (`{runid, type:'settled', by:<finder>, reason:'process gone', spentUsd:<floor>}`) and
3589
+ counts its real spend. If the sum fits, the run goes; if not, it appends `{type:'released',
3590
+ 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
3592
+ unreadable config, refuses too — never "no limit". When a run ends it appends its own
3593
+ `{runid, type:'settled', by:<runid>, spentUsd, spendComplete, at}`. Two runs may close the same dead claim;
3594
+ the FIRST note for a runid is authoritative. `readRunList` folds these entries out of the run rows
3595
+ (`events`; a `released` claim's row is not listed). Month totals still read real spend off the spines. An
3596
+ 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
3598
+ safe-side cost of doing without a lock.
3599
+ Only the refusal reserves the cap (`monthSpend().reservedUsd`); the Money tab's month figure is
3600
+ real spend.
3601
+
3602
+ **Money & limits tab** (`GET/POST /api/settings/money`): two tiles ($ and tokens, this month / to date, a
3603
+ `≥` prefix when a figure is a floor) and a per-provider table ($, minutes, tokens; a run is grouped
3604
+ by the provider on its `job-start`, an older spine by its model). The limit field auto-saves on
3605
+ change (no Save button); blank clears it. `POST` needs an explicit `monthlyLimitUsd` key (a number
3606
+ above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, never "clear the limit".
3607
+
3608
+ **Providers tab (P4b, `src/providerrows.js`).** One row per key in `.env` that HAS A VALUE — nothing is
3609
+ hardcoded; an empty line is no row, a removed line drops its row on Reload keys. A missing `.env` is
3610
+ created (mode 600) on the first Providers read with five empty preset lines (`ANTHROPIC_API_KEY`,
3611
+ `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `LOCAL_API_KEY`); an existing file is never
3612
+ edited. A value of `null` counts as SET and means "no real key" (the provider is handed the
3613
+ placeholder `no-key-needed`, never the word `null`). Each row: the env NAME (read-only) + found,
3614
+ **Name** (the model id Chat uses), **API shape** (Anthropic / OpenAI-compatible / Gemini), **Base
3615
+ URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display only), and Balance
3616
+ (DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
3617
+ `keys.<ENV NAME> = { name, shape, baseUrl }`. Defaults for a key with no saved entry:
3618
+ `ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
3619
+ OpenAI-compatible / `https://api.deepseek.com/v1`; `OPENAI_API_KEY` OpenAI-compatible;
3620
+ `GEMINI_API_KEY` Gemini; `LOCAL_API_KEY` OpenAI-compatible / `http://127.0.0.1:11434/v1` (Ollama, LM
3621
+ Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **No
3622
+ price is shown anywhere** (display only — cost recording, the estimated provenance, spend sums, caps
3623
+ and the monthly limit are unchanged; a local model has no known rate, so its rounds come out as a
3624
+ non-zero loud guess, never a real $0).
3625
+
3626
+ `providerrows.js` is the ONE owner of "model → key + shape + URL": `keyRows`, `findRow` (rows on the
3627
+ same shape + endpoint; the Name breaks a tie), `modelChoiceFor` / `chatModels` (Chat's Model menu IS
3628
+ the rows; a blank Name is not offered), `keyNameFor` / `applyConfiguredKey` (a job on `(provider,
3629
+ baseUrl[, model])` reads the matching row's variable, else the provider's built-in one). All doors
3630
+ (`run-u`, `interview`, `author`, the panel's drafting) use it. Routes (all behind the per-start token
3631
+ and Origin check; the chat can never reach them): `GET /api/settings/providers`, `POST .../row`
3632
+ (`{envName, name, shape, baseUrl}`), `POST .../balance-note`, `POST .../test` (one
3633
+ `checkProviderReachable` models-list GET at the row's shape + URL, $0; a refused connection reads
3634
+ `nothing answering at <URL>`), `GET .../balance?env=`
3635
+ (DeepSeek-hosted row, server-side), and `GET /api/author/models` (Chat's Model menu). The old
3636
+ `providers.<row>.key` config field and the key dropdown are gone. Like the authoring routes, a POST
3637
+ body over 1 MiB is refused `413`.
3638
+
3493
3639
  ### The source front door — a plain folder/file/URL, no `--patient` (PRD item 33/M2, `src/source.js`)
3494
3640
 
3495
3641
  A repo job still gives `runJob` a `--patient` checkout it owns. A NON-code job (a resume, a
@@ -3724,7 +3870,7 @@ coldReset, the close-first precheck — is untouched: `--spec` only NAMES the jo
3724
3870
  a `--job` row does. `src/authorrun.js`'s (lifted out of `scripts/run-author.mjs` by
3725
3871
  PANEL-BUILD.md P0) "SIGNING PREPARED — NOT SIGNED" screen now
3726
3872
  prints the ready-to-paste `--spec` command (real path, real `--approve <hash>`, the
3727
- provider's real env-key per F187) — a person finishing the interview alone can reach a
3873
+ key variable the job's key row really reads (`keyNameFor`, F187/F207)) — a person finishing the interview alone can reach a
3728
3874
  running job with no `jobs/` edit and no JOBS-table row.
3729
3875
 
3730
3876
  ## Architecture
@@ -3762,9 +3908,10 @@ merge.** What an adopter can plug into `runJob` (`src/run.js:205`):
3762
3908
  `emit(type, data)` that appends one JSONL line per event; any function with that
3763
3909
  signature can stand in as a custom sink, but it is write-only — nothing in bareloop reads
3764
3910
  the spine back, so a custom sink cannot feed anything into a run's own decisions.
3765
- - **`rates` is NOT an extension point.** A rates passthrough was designed but never built;
3766
- pricing is guesstimate-plus-loud-`estimated`-flag by ruling, and rate tables are the
3767
- customer's own responsibility, not a bareloop seam to wire up.
3911
+ - **`rates` is data the run doors hand in, not a behaviour hook.** `runJob` / `runPlan` take
3912
+ `rates` / `judgeRates` (`{in, out}`, USD per 1K) resolved from the customer's own price on a key
3913
+ row (`ratesFor`, `src/providerrows.js`); bareloop keeps no price table and never picks a price
3914
+ itself. A price is the customer's, and sets what the cap means.
3768
3915
 
3769
3916
  Everything else in `runJob`'s options — budgets, caps, the close, the fence, merge/publish
3770
3917
  — is arbiter territory and is never adopter-suppliable as behavior, only as signed,
package/bin/bareloop.mjs CHANGED
@@ -15,7 +15,6 @@ process.stdout.on('error', (e) => {
15
15
  });
16
16
 
17
17
  const code = await main(process.argv.slice(2), {
18
- env: process.env,
19
18
  stdout: process.stdout,
20
19
  stderr: process.stderr,
21
20
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.31.0",
3
+ "version": "0.33.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/authorflow.js CHANGED
@@ -1438,12 +1438,14 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
1438
1438
  * an estimate, and it is why tool mode does not cost double what text mode did.
1439
1439
  *
1440
1440
  * `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
1441
- * @param {any} provider @param {{system?: string, maxTokens?: number}} [o]
1441
+ * `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
1442
+ * absent = bare-agent's built-in guess, exactly as before.
1443
+ * @param {any} provider @param {{system?: string, maxTokens?: number, rates?: {in: number, out: number}|null}} [o]
1442
1444
  * @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
1443
1445
  */
1444
- export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens = AUTHOR_MAX_TOKENS } = {}) {
1446
+ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens = AUTHOR_MAX_TOKENS, rates = null } = {}) {
1445
1447
  return async (/** @type {any[]} */ messages, /** @type {any[]} */ tools, /** @type {any} */ opts = {}) => {
1446
- const loop = new Loop({ provider, system });
1448
+ const loop = new Loop({ provider, system, ...(rates ? { rates } : {}) });
1447
1449
  const wired = tools.map((t) => ({
1448
1450
  ...t,
1449
1451
  execute: async (/** @type {any} */ a) => { const r = await t.execute(a); loop.stop(); return r; },
package/src/authorjob.js CHANGED
@@ -419,6 +419,7 @@ function composerRefusal(reds) {
419
419
  * verdictType?: string|null,
420
420
  * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
421
421
  * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null, judgeModel?: string|null,
422
+ * rates?: {in: number, out: number}|null,
422
423
  * onPhase?: (phase: string, data?: any) => void,
423
424
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
424
425
  * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
@@ -435,6 +436,7 @@ export async function authorCloseForJob({
435
436
  answers, repoPath = null, lang, verdictType = null, questions = null,
436
437
  generate, provider = null, seedRef = null, scout = null, listing = null,
437
438
  ceilingUsd = null,
439
+ rates = null,
438
440
  // Q2 IS GONE from the numbered interview (PRD item 33 M3 piece 3) — the
439
441
  // fence it used to describe in prose is now Destination's own proven
440
442
  // `writeScope`, and this is what carries it into the composer prompt
@@ -578,7 +580,7 @@ export async function authorCloseForJob({
578
580
  // real tree. `attempts` is the ceiling the scout enforces on itself, so the
579
581
  // reader knows how many the line could repeat for.
580
582
  onPhase('scout', { attempts: SCOUT_ATTEMPTS });
581
- survey = await scoutFn({ workdir, provider, ceilingUsd, onCall });
583
+ survey = await scoutFn({ workdir, provider, ceilingUsd, onCall, ...(rates ? { rates } : {}) });
582
584
  // …and the survey's own VERDICT, said the moment it lands. `state` is what
583
585
  // every downstream refusal keys on (F59: absent is never "no facts needed"),
584
586
  // and the fact count is the one number that says whether the ladder came