bareloop 0.40.0 → 0.41.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 (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bareloop.context.md +97 -124
  3. package/package.json +2 -2
  4. package/src/authorflow.js +9 -9
  5. package/src/authorjob.js +4 -4
  6. package/src/authorrun.js +25 -25
  7. package/src/authorscout.js +4 -4
  8. package/src/bundle.js +7 -4
  9. package/src/bundlerun.js +4 -1
  10. package/src/cli.js +1 -1
  11. package/src/config.js +129 -29
  12. package/src/draftspend.js +4 -4
  13. package/src/index.js +2 -4
  14. package/src/interviewrun.js +17 -25
  15. package/src/job.js +13 -7
  16. package/src/judged.js +11 -43
  17. package/src/keysfile.js +15 -7
  18. package/src/kinds.js +2 -2
  19. package/src/modellist.js +84 -0
  20. package/src/monthly.js +23 -43
  21. package/src/panel/authorroutes.js +19 -14
  22. package/src/panel/authorsession.js +62 -58
  23. package/src/panel/index.html +520 -145
  24. package/src/panel/runroutes.js +3 -0
  25. package/src/panel/server.js +35 -15
  26. package/src/panel/settingsroutes.js +232 -173
  27. package/src/planrun.js +2 -2
  28. package/src/providerrows.js +226 -141
  29. package/src/providers.js +58 -332
  30. package/src/providershapes.js +18 -0
  31. package/src/replay.js +1 -1
  32. package/src/thinking.js +130 -0
  33. package/src/userrun.js +50 -73
  34. package/types/authorflow.d.ts +5 -5
  35. package/types/authorjob.d.ts +2 -2
  36. package/types/authorscout.d.ts +2 -2
  37. package/types/config.d.ts +23 -6
  38. package/types/draftspend.d.ts +8 -8
  39. package/types/index.d.ts +1 -1
  40. package/types/job.d.ts +5 -5
  41. package/types/judged.d.ts +5 -36
  42. package/types/keysfile.d.ts +15 -4
  43. package/types/modellist.d.ts +50 -0
  44. package/types/monthly.d.ts +4 -3
  45. package/types/panel/authorsession.d.ts +2 -9
  46. package/types/panel/server.d.ts +14 -4
  47. package/types/providerrows.d.ts +192 -99
  48. package/types/providers.d.ts +34 -159
  49. package/types/providershapes.d.ts +10 -0
  50. package/types/thinking.d.ts +90 -0
  51. package/types/userrun.d.ts +6 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ 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.41.0] - 2026-10-12
9
+
10
+ ### Added
11
+
12
+ - **Settings > Providers, rewritten from fwdloop's layout (P8).** One shared grid: a key row (API shape, Base URL, the "old max_tokens" tick, Tokens, `[ Test ] [ ⟳ Refresh ]`) with its model lines under it (Use tick, In / Out $/1M, tokens), a folded summary, `label : value` lines below 1280px, and the KEYS strip stacking below 900px. **Test** and **Refresh** are one free `GET <Base URL>/models` (`src/modellist.js`); `gemini-api` is now testable. `Open keys folder` stays in the KEYS strip.
13
+ - **Model lines.** Config gains `models.<ENV>` — a list of `{ model, use, inPerM, outPerM }` — and `keys.<ENV>` becomes `{ shape, baseUrl, legacyMaxTokens }`. A price is a pair or nothing, a ticked line needs both, 0 / 0 is a chosen price. An older `keys.<ENV>.{name,priceInPerM,priceOutPerM}` still reads as one line until the row's first save.
14
+ - **"Thinking on" tick (per job).** A tick under Model in the New job card, unticked by default, saved in the signed job as the optional `thinking` boolean (absent = off; an old spec and any unticked job keep their hash). It covers drafting and the rubric judge, never the worker. `src/thinking.js` is the one owner; bare-agent (^0.52.0) says what each model can switch and reports `thinkingStatus`, which the draft log and the drafting `thinking` row show.
15
+ - **`--model <id>` and `--provider <KEY>`** on `run-u`, `run <bundle>` and the resume route name the Settings line a run works on (for a spec that names no model); `interview` takes `--model` and writes it into the draft.
16
+ - `tests/no-hardcode.test.js`: a search of `src/` and `bin/` finds no provider name, model name, web address or price in code, except an allow-list with a reason per entry.
17
+
18
+ ### Changed
19
+
20
+ - **Resume of a pre-P8 run whose signed spec names no model** opens the Resume card's Model box (it was locked, so the person could not act on `--model`). The menu offers only lines of the spec's API shape (and endpoint, when the spec names one) and shows `Tick a <shape> model in Settings > Providers.` when none match; Sign & resume stays blocked until a usable line is picked, and the pick is sent as model + provider. The spec and its hash are untouched. Changing the Model line now blocks Start/Sign until its key check returns, and a late answer for a line no longer selected is ignored (the New job card shares this path).
21
+ - **Chat's Model menu** lists only ticked, priced lines as `<KEY> · <model>`; a card carries `provider` (the key's env name) beside `model`, and a stale pick is refused at $0 with one of three plain sentences.
22
+ - **A job's key is its row's own variable name.** There is no built-in key name, tier table, default model, default address or per-model gating: the "old max_tokens" tick on the row decides the output-cap spelling.
23
+ - **Existing DeepSeek rows need one tick.** An existing DeepSeek row (or any OpenAI-compatible server that wants the old `max_tokens` field) must have "old max_tokens" ticked once in Settings > Providers: the built-in per-model rule that did this is gone (no-hardcode ruling), and until the tick is set the output cap may not bind (F149).
24
+ - The Money tab groups spend by the Providers row that served it (a run no row serves lands under its own shape and address) and carries tokens per model.
25
+ - A fresh keys file holds two comment lines and no preset key.
26
+ - Dependencies: `bare-agent` ^0.51.0 to ^0.52.0.
27
+ - The exported bundle README tells the user to put their provider's API key in their keys file (the name their Settings row uses), with no vendor.
28
+
29
+ ### Removed
30
+
31
+ - **Balance** (the Settings column, both routes and the `anthropicBalanceNote` config field).
32
+ - Built-in tiers and keys: `*_TIER_MODELS`, `envKey`, `OPENAI_MODEL_OPTIONS`/`paramsFor`, `PRESET_KEY_NAMES`, the vendor default addresses, the `--model sonnet|haiku` aliases, the vendor map in the Money tab.
33
+ - **`JUDGE_MODEL`** is no longer exported from the package root or `src/judged.js` (a public-API removal: pass your own model id). `resolveWorkerModel` takes `suppliedModel` instead of `defaultModel` and throws when no model is named.
34
+ - The bench row `bareguard-types-deepseek` is renamed `bareguard-types-gateway` (spec file renamed; contents and hash untouched; the patient directory it names is now `bareloop-patients/bareguard-u-gateway`).
35
+
36
+ ### Breaking
37
+
38
+ - **The config shape changed.** Every row that relied on a built-in default (shape, address, model, price) must have its API shape, Base URL and model lines entered or ticked ONCE in Settings. A row on a server that ignores `max_completion_tokens` (F149) needs "old max_tokens" ticked by hand, once, before a paid run: an output cap that does not bind is a money hazard.
39
+ - **Specs signed before this release that name no `model` (or no `baseUrl`)** have nothing to run on until a line is named at run start: `--model <id>` (the hash is untouched). A signed model no ticked line carries is refused at $0.
40
+
8
41
  ## [0.40.0] - 2026-10-11
9
42
 
10
43
  ### Added
@@ -56,20 +56,16 @@ inherited rule carries the green that minted it and the contrast that attributed
56
56
  with `anthropic-api` on cost, and budgets don't transfer between them.
57
57
  `job.provider === 'clipipe-subscription'` with no `nativeProvider` supplied is
58
58
  `interpreter-red`, never a silent fall-back to the metered API (`src/planrun.js:1073`).
59
- `provider: 'openai-api'` (PRD item 28) takes the ordinary `Loop` path against an
60
- OpenAI-shaped endpoint, reads `OPENAI_API_KEY`, and accepts an optional job-level
61
- `baseUrl` (https, or http on loopback only; credentials in the URL are a red). Its one
62
- admitted model is `deepseek-flash`, which earned the slot with a real paid green under
63
- its retired predecessor `deepseek-chat` (F171: no longer served as of 2026-09-13,
64
- swapped to `deepseek-flash`) — no endpoint or model enters the menu without its own
65
- clean paid probe. The provider table
66
- lives in `src/providers.js` (`resolveProvider`/`makeProvider`/`buildRunnerProviders`);
67
- an unknown provider name THROWS there, it never falls back to a default. Per-model
68
- request-key gating lives in that table: `deepseek-flash` sets bare-agent's
69
- `legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
70
- output cap that does not bind is a money hazard. The judge's calls additionally carry the per-call `thinkingMode: 'off'` where bare-agent's `thinkingSupport` says the model is switchable (read through `judgeCallOptions`, applied by `defaultJudgeLoop`; F192) because deepseek-flash's default thinking ate the 4000-token locate budget; workers are untouched. `provider: 'gemini-api'` (PRD item 31.3)
71
- takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
72
- (`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
59
+ `provider` names an API SHAPE — a wire protocol, never a vendor: `anthropic-api`, `openai-api` (an OpenAI-compatible endpoint, with an optional job-level
60
+ `baseUrl`: https, or http on loopback only; credentials in the URL are a red) or `gemini-api`. Nothing about a vendor, a model id, an address, a price or a
61
+ key name is built in (P8): a job's WORKER runs on a **Settings line** — a key row (its API shape, Base URL and "old max_tokens" tick) with a model line the
62
+ person ticked and priced (see "Settings > Providers" below). The provider table lives in `src/providers.js` (`resolveProvider`/`makeProvider`/
63
+ `buildRunnerProviders`) and holds only the bare-agent client class and endpoint option name per shape; an unknown provider name THROWS there, it never falls
64
+ back to a default. The request-key spelling is the ROW's tick, never a model-id table: a server that ignores `max_completion_tokens` (F149) does not bind
65
+ the output cap until the "old max_tokens" box is ticked, and an output cap that does not bind is a money hazard, so tick it on that row before a paid run
66
+ (`makeProvider(shape, { legacyMaxTokens })`, forwarded for `openai-api` only). The judge's calls carry the per-call `thinkingMode` the job's signed "Thinking on" tick asks for
67
+ (`src/thinking.js`, applied by `defaultJudgeLoop`; F192: unticked = off, because a model's default thinking can eat the 4000-token locate budget); workers are untouched. A plan step's
68
+ `model: "sonnet"` is a documented schema token meaning "this job's worker model" (`STEP_MODELS`), not a model id. `gemini-api` (PRD item 31.3) is
73
69
  **ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
74
70
  `PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
75
71
  `probeWarningLines(provider)` renders the loud launch-time marker `src/userrun.js`
@@ -85,11 +81,11 @@ inherited rule carries the green that minted it and the contrast that attributed
85
81
  at $0 through machinery that treats $0 as a real price. The JUDGE IS NO LONGER PINNED
86
82
  (PRD item 32): `resolveJudge` (exported) resolves it per job — the spec's signed
87
83
  `judge: {provider, model}` if it names one, else **this job's own worker provider and
88
- model**, so a DeepSeek or Gemini job judges on its own provider and needs no second
84
+ model**, so a job on any provider judges on its own provider and needs no second
89
85
  account. There is no library fall-back: a run that cannot name its judge has not wired
90
86
  one, and the judged stage instrument-STOPS rather than grading under an identity nobody
91
- chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin — no grading path reads it.
92
- The judge's key follows the resolved judge provider's own `envKey`, with `JUDGE_API_KEY`
87
+ chose. The old `JUDGE_MODEL` constant is gone (a vendor model id is not a public export).
88
+ The judge's key is the resolved judge row's own variable (the worker's row when the judge shares its shape), with `JUDGE_API_KEY`
93
89
  as a role-named override in front of it. The bundle runner
94
90
  (`bareloop run`) is a door to this same engine (`src/bundlerun.js` → `src/userrun.js`), so it
95
91
  reads whatever key the bundle's provider names (keys file or env) and refuses a missing one
@@ -155,13 +151,14 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
155
151
  |---|---|---|
156
152
  | `job` | kebab-case slug | |
157
153
  | `description` | non-empty string | |
158
- | `provider` | `anthropic-api` \| `openai-api` \| `gemini-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-flash` today (F171: swapped from the retired `deepseek-chat`) — see "Worker surface" above for its `baseUrl` field and per-model gating; `gemini-api` (PRD item 31.3) reads `GEMINI_API_KEY` and is ADMITTED-PENDING-PROBE — zero runs, and every launch says so (`PROBE_STATUS`/`probeWarningLines`); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
154
+ | `provider` | `anthropic-api` \| `openai-api` \| `gemini-api` \| `clipipe-subscription` | menu (`PROVIDERS`): an API SHAPE (a wire protocol), part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); the shapes go through the provider factory (`src/providers.js`) — see "Worker surface" above for the `baseUrl` field and the row's old-max_tokens tick; `gemini-api` (PRD item 31.3) is ADMITTED-PENDING-PROBE — zero runs, and every launch says so (`PROBE_STATUS`/`probeWarningLines`); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
159
155
  | `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
160
156
  | `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
161
157
  | `budgetUsd` | finite `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
162
158
  | `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
163
159
  | `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
164
- | `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the JUDGE's model comes from the `judge` row below, which DEFAULTS to this same worker model |
160
+ | `model` | optional non-empty string, the exact model id of a line ticked in Settings > Providers | the WORKER's model. A job carries its own model (P8: bareloop has no default); a spec signed before that (no `model`, perhaps no `baseUrl`) runs on the line `--model <id>` names (plus `--provider <KEY>` when two keys tick it) — the signed hash is untouched. Present, it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the JUDGE's model comes from the `judge` row below, which DEFAULTS to this same worker model |
161
+ | `thinking` | optional boolean | the job's "Thinking on" tick (P8): absent = off, and an old spec keeps the hash it always had; present it is signed like `model`. Covers DRAFTING and the rubric JUDGE only — the worker is never handed a thinking option (see "Thinking on" below) |
165
162
  | `judge` | optional `{provider, model}`, both non-empty; `provider` from the `PROVIDERS` menu | the signed JUDGE override (PRD item 32). Absent, the judge resolves to this job's own `provider`/`model` (`resolveJudge`) — which is what lets a non-Anthropic job judge without a second account. Present, it pins a judge deliberately different from the worker (an independent reader). Both halves are required together: a judge with a provider and no model is an identity nobody signed. Part of the signed hash, so changing it kills the signature and forces recalibration. Unknown nested keys red — no smuggling level exists in a signed spec. The drafting agent cannot express it: naming your own examiner is arbiter territory |
166
163
  | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
167
164
  | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
@@ -363,7 +360,7 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
363
360
  | `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
364
361
  | `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
365
362
  | `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
366
- | `steps[].model` | optional; `sonnet` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable; `haiku` was removed 2026-08-06 as a reversible ATTRIBUTION PROBE (F87, see `STEP_MODELS`), so the menu is one entry deep and the field is no longer offered in the drafting prompt. The field and its validator branch STAY: `model: "sonnet"` still validates, a stored bridge declaring it still loads, and restoring haiku is a one-token edit — this is not a finding that haiku is incapable. **The narrowing binds only what the AGENT may express.** Which real model a tier maps to, and what the caller runs as its own default worker model, stay the OPERATOR's: `providerFor` is yours, and the in-repo runner's `--model haiku` probe knob is untouched (PRD v1.36) — the runner's tier map is deliberately WIDER than the plan menu. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
363
+ | `steps[].model` | optional; `sonnet` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (`sonnet` is a schema token meaning this job's worker model; the runner maps it, there is no tier table). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable; `haiku` was removed 2026-08-06 as a reversible ATTRIBUTION PROBE (F87, see `STEP_MODELS`), so the menu is one entry deep and the field is no longer offered in the drafting prompt. The field and its validator branch STAY: `model: "sonnet"` still validates, a stored bridge declaring it still loads, and restoring haiku is a one-token edit — this is not a finding that haiku is incapable. **The narrowing binds only what the AGENT may express.** Which real model a tier maps to, and what the caller runs as its own default worker model, stay the OPERATOR's: `providerFor` is yours, and the in-repo runner's `--model haiku` probe knob is untouched (PRD v1.36) — the runner's tier map is deliberately WIDER than the plan menu. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
367
364
  | `steps[].attempts` | optional; int `>= 1` | **RETIRED and INERT (F79) — it bounds nothing.** A step's iterations are governed by the STRIKE LADDER (below), which is shell-owned and inexpressible from the plan. The field is no longer offered in the drafting prompt and the runner ignores it; it is deliberately still ACCEPTED, because stored bridge plans carry it and a plan minted under one runner's number must not red under another's. The shape is still checked (a known field holding garbage stays a `bounds` red); the old `<= capRuns` ceiling is gone with the cap it named. It was previously TIGHTEN-only — which meant a step that needed MORE iterations could not ask, and drafters measurably tightened it instead (F77) |
368
365
  | `steps[].scope` | optional; one value from the SAME menu `tree-changed` uses | narrows this step's live WRITE fence to a subset of the signed `writeScope` — chosen from the offered scope menu (below), never authored. Off-menu reds `invalid-value` carrying the menu |
369
366
  | `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. **`tree-changed.scope` is a MENU, not a glob the agent authors** — the runner enumerates the signed fence entries plus the real directories beneath them (shallowest-first, capped at `MAX_SCOPE_MENU`=24; `legalScopes` in `src/plan.js`), `planPrompt` lists them verbatim, and `validatePlan` accepts membership only — the SAME array on both sides, so what was offered is what is accepted; omitted, it derives from `writeScope` alone — never a free-text fallback. An off-menu value that escapes the fence still reds `scope-escape` (the ledger's attribution class), an in-fence unoffered value reds `invalid-value` carrying the menu. `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` so a capped menu is never silently complete. `check-passes` must name a stage on the close's DERIVED menu (`check-unknown` red names the offered menu — no operator authors this list, it comes straight from `checkMenu(close)`); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). **`check-passes` also requires a write-class verb (`write`/`edit`) on the SAME step** (`exit-illegal` — BREAKING for plan authors: a read-only "verify" step carrying a check used to validate and no longer does). A failing check's gap is re-delivered to that step's OWN worker, so a step holding a check must be able to act on it; a read-only one is a mailbox with no hands and stalls to cap on a byte-identical gap (measured on 4 of 4 drafted plans). The check belongs on the step that fixes; the run's final verification is the operator's close, which the agent never authors. The rule is stated identically in the drafter prompt and the validator, so the two can never disagree — and it is suppressed when `tools` failed to parse (the step's hands are unknowable then, and the real defect already redded: one defect, one red). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
@@ -647,26 +644,29 @@ plan with the first leg's spend carried as prior spend and `offerQuestions: fals
647
644
  plan's open questions) are a separate, unchanged mechanism: they are asked before drafting, about the plan; try-3 questions
648
645
  are asked after two failed drafts, about a job line.
649
646
 
650
- **Drafter thinking knob (hamr 2026-10-10; DEFAULT OFF, hamr 2026-10-11; via bare-agent's switch since v0.51).** `draftThinking: 'on' | 'off'` (any other
651
- value throws, an operator param, never read from env inside the library, not in the panel UI) is owned by `draftCallOptions(provider, mode)`
652
- (`src/providers.js`). bareloop keeps NO per-model thinking list: bare-agent >= 0.51 owns the request shapes (`thinkingMode: 'on' | 'off'`, per provider
653
- and model) and `thinkingSupport(kind, model)` (pure lookup, no network) says whether a model can be switched. The default is `'off'`. The frozen ON/OFF contrast
654
- (`docs/logs/2026-10-10-drafter-thinking-contrast-prereg.md`) measured deepseek-flash only; the default applies to every switchable model, drafting and
655
- judge alike, by hamr's 2026-10-11 ruling (no per-model exceptions), and is not measured live on other providers (Anthropic, Gemini, OpenAI). With mode `'off'`, a call carries the per-CALL option
656
- `thinkingMode: 'off'` only when `thinkingSupport` answers `switchable: true` (e.g. deepseek-flash, claude-sonnet-5, gpt-5); a model measured NOT switchable
657
- (claude-opus-5-5) gets nothing (asking would throw before send), and an `'unverified'` model (not measured by bare-agent) also gets nothing on this
658
- default path, so an unknown or local model's drafting never breaks by default. `'on'` sends nothing (the model's own thinking, byte-identical to a call with
659
- no knob). The raw `thinking` option is never sent. It is threaded explicitly: `makeLoopGenerate(provider, { draftThinking })` for the author, revise,
660
- structure-retry and confirm calls; `authorCloseForJob({ draftThinking })` for the scout (it hands `runAuthorScout` a `callOptions`);
661
- `createSession(card, { draftThinking })` in the panel does both. The CLI drafter (`src/authorrun.js`) passes no value, so it gets the same default
662
- through the same two seams. The rubric judge uses the same rule (`judgeCallOptions`: `thinkingMode: 'off'` only where switchable; a judge on
663
- claude-opus-5-5 does not throw). Worker calls are unchanged. Each panel `draft-log.jsonl` `call` entry records `thinking` = what was asked and sent
664
- (`draftThinkingSent`): `'off'` (the switch was sent), `'on'` (mode on, nothing sent), `'not-switchable'` (measured not switchable, nothing sent) or
665
- `'unverified'` (not measured, nothing sent); plus `thinkingApplied` (bare-agent's own verdict for that call's run: `true` / `false` = reasoning seen
666
- although `'off'` was sent / `null` = unreadable) when thinkingMode was sent, absent otherwise. The panel's drafting list opens with a code-owned
667
- `thinking` row (`thinkingRowText`, shown with a check, before "checking setup"): `thinking off`, `thinking on`, `thinking on (this model can't turn it off)`,
668
- `thinking on (not verified for this model)`, or, once any call reports `thinkingApplied: false` after `'off'` was sent, `thinking on (the model thought
669
- anyway)` (a `step-label` record in the draft log carries the change). A Reuse workflow session drafts nothing and has no row.
647
+ **Thinking on (per job; hamr 2026-10-11).** The New job card has a "Thinking on" tick under Model, unticked by default, saved in the SIGNED job as the optional
648
+ boolean `thinking` (absent = off; an old spec and any unticked job keep the hash they would have had; a ticked job's `jobSpecHash` and `workflowKey` differ). It
649
+ covers DRAFTING (scout, confirm, author, revise, structure retries) AND the rubric JUDGE; the WORKER is never handed a thinking option. A Reuse card does not
650
+ offer the tick: it never drafts and the judge follows the SIGNED value, so the 10/10 calibration gate runs with the same tick as the later grading
651
+ (`defaultJudgeLoop({ ..., thinking })` takes it in all three constructors: the run, `run-author`, the panel's gate). bareloop keeps NO per-model thinking list and no
652
+ vendor regex: bare-agent (>= 0.52) owns the request shapes, and `thinkingSupport(kind, model, { baseUrl })` (a pure lookup, no network) answers `true` (measured
653
+ switchable), `false` (measured NOT switchable) or `'unverified'`. `src/thinking.js` is the ONE owner of what a call sends:
654
+
655
+ | tick | support `true` | support `false` | support `'unverified'` |
656
+ |---|---|---|---|
657
+ | on | `{thinkingMode:'on'}` | `{}` (the box is greyed and cannot be on) | `{thinkingMode:'on'}` |
658
+ | off | `{thinkingMode:'off'}` | `{}` (the model thinks as it would) | `{thinkingMode:'off'}` (bare-agent decides) |
659
+
660
+ bare-agent 0.52 never throws because a model cannot switch: the call always runs and reports `GenerateResult.thinkingStatus` (`'applied'` / `'unsupported'` /
661
+ `'ignored'` / `'unknown'`; `Loop.run()` surfaces the worst across rounds). For an unmeasured model it tries the other shapes, then ONE plain request
662
+ (`'unsupported'`, the result kept), so drafting does not break. The raw `thinking` option is never sent. The per-call option is threaded through
663
+ `makeLoopGenerate(provider, { draftThinking: 'on' | 'off' })` and `authorCloseForJob({ draftThinking })` (any other value throws, an operator param); the panel builds it
664
+ from `card.thinking`. `GET /api/author/models` answers each Model line with `thinking: { support, canChange, say }`: the page decides nothing: `canChange:false` greys
665
+ AND unticks the box with `say` ("can't change this for this model"); `'unverified'` leaves it enabled with no note. Each panel `draft-log.jsonl` `call` entry records
666
+ `thinking` (`'on'` / `'off'`, the tick), `thinkingSupport` (`true` / `false` / `'unverified'`) and, when bare-agent reported one, `thinkingStatus`. The drafting list opens
667
+ with a code-owned `thinking` row (`thinkingRowText`, before "checking setup"): `thinking off` / `thinking on`; `thinking off (not verified for this model)`;
668
+ `thinking on (this model can't change it)`; and, once a call reports a status, `thinking on (the model thought anyway)` (`'ignored'`), `thinking on (this model can't change it)`
669
+ (`'unsupported'`) or `thinking off (not confirmed)` (`'unknown'`) — a `step-label` record in the draft log carries the change. A Reuse workflow session drafts nothing and has no row.
670
670
 
671
671
 
672
672
  **D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
@@ -950,13 +950,12 @@ down.
950
950
  `--base-url` is OPTIONAL with NO default (PRD item 33 close-out — L17 named the provider but
951
951
  never admitted the endpoint): a provider name's table entry (`src/providers.js`) can be reached
952
952
  through an OpenAI-compatible gateway other than its own default host — DeepSeek, today's one
953
- secondary, is reached as `--provider openai-api --base-url https://api.deepseek.com/v1`. Absent,
953
+ secondary, is reached as `--provider openai-api --base-url <the endpoint>`; `--model <id>` names the Settings line the job runs on and lands in the draft's `model`. Absent,
954
954
  the draft carries NO `baseUrl` field at all (never `null`, never `''`) and every provider
955
955
  constructor defaults its own endpoint. When given, it lands in the draft's `baseUrl` field
956
956
  beside `provider` and passes through the SAME `validateJob` pass every other field takes below,
957
957
  at $0: `http://` to a public host, an embedded `user:pass@`, or an unparseable string reds there,
958
- before anything is written. The key still follows the PROVIDER, not the endpoint — a DeepSeek
959
- draft's key still loads from `OPENAI_API_KEY`, and the hand-off says so.
958
+ before anything is written. The key is the Settings ROW's variable that carries the model line — the hand-off names it.
960
959
 
961
960
  **Source and Destination are the interview's own FIRST TWO QUESTIONS, and they REPLACE
962
961
  `--patient`** (PRD item 33 M3, ruling 2, `docs/product/ITEM33-BUILD.md`). Source — "a local
@@ -1021,8 +1020,8 @@ spends it.
1021
1020
 
1022
1021
  **It ends by OFFERING the paid step, and the offer's default is NO.** `run-author.mjs` is a
1023
1022
  DIFFERENT PROCESS under its own ceiling; the interview prints its exact command line (with
1024
- `--budget` propagated only when one was given, and a note when the CHOSEN provider's own key —
1025
- `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, resolved through the same provider table
1023
+ `--budget` propagated only when one was given, and a note when the CHOSEN row's own key — the
1024
+ variable named by the Settings row that carries the model line, resolved through the same rows
1026
1025
  `run-author.mjs` and `run-u.mjs` share — is unset in this shell, OR set but MALFORMED: F181 — a
1027
1026
  value carrying a line break/control character/stray whitespace (a two-line secret-store entry,
1028
1027
  e.g.) reads a distinct reason via `apiKeyProblem` (`src/providers.js`), never the value)
@@ -1834,9 +1833,7 @@ omitted is the cold path and byte-identical to a run nobody resumed.
1834
1833
  F87; the field stays legal and one-token reversible, and the narrowing binds only what the
1835
1834
  AGENT may express, never what the operator may run).
1836
1835
  The tier menu is signed into the schema; mapping a tier
1837
- onto a real model (and any per-model provider params) is the RUNNER's territory — nothing
1838
- forces the `sonnet` tier onto any particular model, and running haiku as your default worker
1839
- model stays an operator choice the plan menu never governed — so both
1836
+ onto a real model (and any per-model provider params) is the RUNNER's territory — the runner maps the one `sonnet` token to the job's worker model (there is no tier table) — so both
1840
1837
  `runJob` and `runPlan` take `providerFor?: (tier: string) => provider`. It is called ONLY for
1841
1838
  a step that declared a tier, once per such step — the scout, the plan drafter and every
1842
1839
  untiered step stay on `opts.provider`, so a caller that never plans tiers can omit it
@@ -1911,14 +1908,14 @@ bare-agent's `Loop` takes `rates: {in, out, cacheReadMult?, cacheWriteMult?}`
1911
1908
  tokens, the two multipliers applying to the input rate for the cache-read and cache-write
1912
1909
  tiers (defaulting to Anthropic's 0.1× / 1.25×). A caller-supplied rate is recorded as
1913
1910
  VOUCHED rather than as a guess, and it silences bare-agent's own guesstimate warning.
1914
- **Where bareloop takes it from:** the CUSTOMER's own price on the key's row in
1915
- `~/.config/bareloop/config.json` (`keys.<ENV NAME>.priceInPerM` / `priceOutPerM`, USD per 1M
1911
+ **Where bareloop takes it from:** the CUSTOMER's own price on the model line in
1912
+ `~/.config/bareloop/config.json` (`models.<ENV NAME>[].inPerM` / `outPerM`, USD per 1M
1916
1913
  tokens — see "Settings" below). `runJob` / `runPlan` accept `rates` (and `judgeRates`) as data and
1917
1914
  hand them to the one `Loop` that drives worker, scout and planner rounds; the run doors resolve
1918
1915
  them once. No price on the row = no `rates` key anywhere = the guesstimate above, byte-identical to
1919
1916
  before. bareloop keeps no price list of its own, ever.
1920
1917
 
1921
- **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.51.0`)
1918
+ **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.52.0`)
1922
1919
  carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
1923
1920
  `src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
1924
1921
  `pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
@@ -3254,7 +3251,7 @@ worktree, before the engine is ever reached.
3254
3251
  some other module's own generic `main`). `cliMain(argv, deps)` is the in-process test/
3255
3252
  integration seam: pass `deps.provider` (and optionally `providerFor`/`judgeProvider`) and the
3256
3253
  real key check is skipped entirely — a scripted provider *is* the run, the
3257
- same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
3254
+ same seam `tests/planrun-*.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
3258
3255
  `now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
3259
3256
 
3260
3257
  Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
@@ -3493,7 +3490,7 @@ key you want first (the panel picks the row by the Model menu).
3493
3490
  (`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
3494
3491
  `authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
3495
3492
  `bareloop interview`/`bareloop author` already make — never a script's own readline loop,
3496
- never a reimplementation of any gate. `GET /api/author/model-check?model=<id>` is the $0 readiness probe the job card runs
3493
+ never a reimplementation of any gate. `GET /api/author/model-check?provider=<KEY>&model=<id>` is the $0 readiness probe the job card runs
3497
3494
  before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
3498
3495
  completion; the page sees the key's NAME and a status word, never the value). `POST
3499
3496
  /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'`:
@@ -3574,8 +3571,9 @@ key you want first (the panel picks the row by the Model menu).
3574
3571
  part stays running; a part that finished before the death keeps its own status. The page's step map is a row of fit-to-text chips that flow and wrap (no truncation, no horizontal scroll), `[sign title]` each, in plan order, with an arrow between neighbours and a leading arrow on a wrapped line; the status word (from the same table) rides in the chip's tooltip, a retried step has a dashed border and a `↻N` tries mark, a leg boundary a dotted right edge, and a click jumps to that part's Audit row (the live part reads `[▶]`); the step cards and Audit rows
3575
3572
  show the word only; no surface says "done".
3576
3573
  **Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
3577
- `checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
3578
- (blank = the signed caps). `404` unknown run; `409` when `resumePlanFor` says the engine would
3574
+ `checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?, model?, provider?}`
3575
+ (blank = the signed caps; `model` / `provider` — a key's env name — are sent only for an older spec that names no model: they ride the
3576
+ spawn as `run-u`'s `--model` / `--provider`, and the signed hash is untouched). `404` unknown run; `409` when `resumePlanFor` says the engine would
3579
3577
  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
3580
3578
  used) — refused at $0 before any write or spawn — or the monthly $
3581
3579
  limit (same library check and text as Sign & run). If a cap changed, the new spec is written as
@@ -3729,11 +3727,11 @@ key you want first (the panel picks the row by the Model menu).
3729
3727
  `resolved-spec.json`, F185) — exactly one, never both, never neither; `--resume
3730
3728
  <runid|path>`; `--door <runid|path>` with `--decide <rerun|accept|pause>` and (`rerun`
3731
3729
  only) `--text <…>`; `--review-door`; `--approve <specHash>`; `--registry <dir>` +
3732
- `--workflow <name>`; `--model <sonnet|haiku>`; `--read-shim <off|cap|diff|all>`; `--scout
3730
+ `--workflow <name>`; `--model <id>` (a model id of a ticked Settings line — for a spec that names none) and `--provider <KEY>` (the key's env name, when two keys tick one id; no tier aliases); `--read-shim <off|cap|diff|all>`; `--scout
3733
3731
  <on|off>`. No key/no injected `deps.provider`: prints the preview (invocation hints, the
3734
- right key name for the job's own provider) and exits `0`, spending nothing — the same
3735
- preview `scripts/run-u.mjs --job <x>` prints with no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
3736
- set. `scripts/run-u.mjs` is unchanged in behaviour: it is the pre-existing thin adapter
3732
+ right key variable — the Settings row's own) and exits `0`, spending nothing. A spec that names no
3733
+ model and no `--model`, or a model no ticked line carries, is refused at $0 with one plain sentence
3734
+ (`Name the model with --model <id>: …` / `<model> is not set up: …`). `scripts/run-u.mjs` is unchanged in behaviour: it is the pre-existing thin adapter
3737
3735
  over this same `src/userrun.js:main`, not rewired to go through `src/cli.js`.
3738
3736
  **Printed commands are spelled the way you entered:** every re-invocation these flows print (the approve
3739
3737
  line, resume, pause, reopen, the interview's run-author line, the author's run-u line) reads `bareloop
@@ -3852,37 +3850,26 @@ Two files in `~/.config/bareloop/`, outside any repo:
3852
3850
  - **`config.json` — every setting, never a key value.** `readConfig({ home })` /
3853
3851
  `updateConfig(patch, { home })` (atomic tmp + rename, mode 600, unknown fields kept).
3854
3852
  Fields: `monthlyLimitUsd` (number above 0; absent or `null` = no limit; any other value present
3855
- is a `ConfigError`, refused like an unreadable file), `anthropicBalanceNote` (a note the
3856
- 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
3853
+ is a `ConfigError`, refused like an unreadable file), `keys.<ENV NAME>` = `{ shape, baseUrl, legacyMaxTokens }` (the Providers rows' settings: an API shape id, the typed Base URL,
3854
+ the "old max_tokens" tick), `models.<ENV NAME>` = a list of `{ model, use, inPerM?, outPerM? }` lines (replaced WHOLE on every save; the customer's own price below). Nothing is built
3855
+ in: a row with nothing saved is shape `openai-api`, no address, no lines. A secret-shaped
3857
3856
  string anywhere in the document is refused on save. An unreadable file is a `problem` string and
3858
3857
  a refused run start ($0), never "no limit". There is no monthly TIME limit (dropped).
3859
3858
 
3860
- **Your own price — `keys.<ENV NAME>.priceInPerM` / `priceOutPerM`.** The customer sets the price of the
3861
- model behind a key, on that key's row, in USD **per 1M tokens** (input and output), for example
3862
- `"keys": { "DEEPSEEK_API_KEY": { "priceInPerM": 0.006, "priceOutPerM": 1.2 } }`. There is no built-in
3863
- price list. Both numbers or neither: each must be a finite number 0 or more, and one alone, a negative,
3864
- a string or `null` refuses the run at $0 (`ConfigError`, exit 2, "Nothing spent.") — a bad price never
3865
- falls back to the guess. A Settings save keeps the two fields (the Settings SCREEN has no price field
3866
- yet; edit `config.json`). `ratesFor(provider, baseUrl, rows, model)` (`src/providerrows.js`) is the one
3867
- lookup, on the SAME row `keyNameFor` picks the key from (`findRow`: shape + endpoint + model), returning
3868
- `null` (no price) or `{ rates: {in, out}, inPerM, outPerM, envName }` with `rates` in bare-agent's
3869
- per-1K unit (`perM / 1000`). The run doors resolve it once, at $0, before the monthly claim and any
3870
- worktree or spine (`src/userrun.js`; `src/authorrun.js`; the panel's authoring session): the worker row
3871
- prices worker, scout and planner rounds; the judge is priced at ITS model's own row — for a different
3872
- provider its own row, for the worker's provider the row on the worker's endpoint named for the judge's model
3873
- (`judgeRatesFor`, `src/providerrows.js`), falling back to the worker's price when the model names no row.
3874
- (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
3875
- no price it stays the built-in guess, as before. The run preview and the run tail print
3876
- `price yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)` when a price is set, and
3877
- nothing when none is; a 0 / 0 price is accepted but the line then ends `— WARNING: this price is $0, so spend and limits read $0`. **The price is yours, and it sets what the cap means:** the cap and the halts run
3878
- on the dollars your price produces, so a price set too low weakens the cap (a run can do far more
3879
- than you meant before it halts). When a vendor lists two prices (for example a cache-hit and a
3880
- cache-miss input rate) enter the higher one. bare-agent applies its own cache multipliers (read 0.1×,
3881
- write 1.25× of the input rate) to cache tokens; they are not configurable here, so cache tokens are
3882
- priced from your input price by that rule. A price applies from the next run: **past runs are never
3883
- re-priced** (a spine and the run list keep the dollars they booked). Not covered by a price: a native
3884
- (CLI) session, which reports its own cost, and the reuse engine (`runReuse` / `selectBridge`), which
3885
- resolves no key row.
3859
+ **Your own price — the model LINE's `inPerM` / `outPerM`.** The customer sets the price of each model they use, on that model's line under its key row, in USD **per 1M tokens**
3860
+ (input and output): `"models": { "MY_KEY": [ { "model": "<id>", "use": true, "inPerM": 0.006, "outPerM": 1.2 } ] }`. There is no built-in price list. A price is a PAIR or nothing: each number
3861
+ a finite number 0 or more (0 / 0 typed is a chosen price), and a ticked line needs both (`modelsProblem`, `src/config.js`, the one rule). A line without both prices cannot be ticked, so it
3862
+ is never in Chat's menu and a run on it refuses at $0 (`<model> is not set up: …`). `ratesFor(provider, baseUrl, rows, model)` (`src/providerrows.js`) reads the line's price (`ratesOfLine`);
3863
+ the run doors resolve the chosen line once, at $0, before the monthly claim and any worktree or spine (`src/userrun.js`; `src/authorrun.js`; the panel's authoring session): the worker line
3864
+ prices worker, scout and planner rounds; the judge is priced at ITS model's own line — for a different provider its own row, for the worker's provider the row on the worker's endpoint
3865
+ with a line named for the judge's model (`judgeRatesFor`), falling back to the worker's price when the model names no line. A priced round is stamped `rateSource:'caller'`
3866
+ (VOUCHED, see `spendProvenance`). The run preview and the run tail print `price yours: in $0.006 / out $1.20 per 1M tokens (MY_KEY row)`; a 0 / 0 price is accepted but the
3867
+ line then ends `— WARNING: this price is $0, so spend and limits read $0`. **The price is yours, and it sets what the cap means:** the cap and the halts run on the dollars your
3868
+ price produces, so a price set too low weakens the cap. When a vendor lists two prices (for example a cache-hit and a cache-miss input rate) enter the higher one. bare-agent
3869
+ applies its own cache multipliers (read 0.1×, write 1.25× of the input rate) to cache tokens; they are not configurable here. A price applies from the next run: **past runs are
3870
+ never re-priced**. Not covered by a price: a native (CLI) session, which reports its own cost, and the reuse engine (`runReuse` / `selectBridge`), which resolves no key row. An
3871
+ older config's `keys.<ENV>.{name,priceInPerM,priceOutPerM}` still reads as ONE line (ticked when both prices are valid numbers, else unticked with no price) until the first Settings
3872
+ save of that row's list removes them.
3886
3873
 
3887
3874
  **Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
3888
3875
  atLeast, heldUsd, heldRuns }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
@@ -3926,45 +3913,31 @@ real spend.
3926
3913
 
3927
3914
  **Money & limits tab** (`GET/POST /api/settings/money`): two tiles ($ and tokens, this month / to date, a
3928
3915
  `≥` prefix when a figure is a floor) and a per-provider table ($, minutes, tokens; a run is grouped
3929
- by the provider on its `job-start`, an older spine by its model). The limit field auto-saves on
3916
+ by the Providers ROW that served it — its key variable name — and a run no row serves lands under its own shape and address). The limit field auto-saves on
3930
3917
  change (no Save button); blank clears it. `POST` needs an explicit `monthlyLimitUsd` key (a number
3931
3918
  above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, never "clear the limit".
3932
3919
 
3933
- **Providers tab (P4b, `src/providerrows.js`).** One row per key in `.env` that HAS A VALUE — nothing is
3934
- hardcoded; an empty line is no row, a removed line drops its row on Reload keys. A missing `.env` is
3935
- created (mode 600) on the first Providers read with five empty preset lines (`ANTHROPIC_API_KEY`,
3936
- `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `LOCAL_API_KEY`); an existing file is never
3937
- edited. A value of `null` counts as SET and means "no real key" (the provider is handed the
3938
- placeholder `no-key-needed`, never the word `null`). Each row: the env NAME (read-only) + found,
3939
- **Name** (the model id Chat uses), **API shape** (Anthropic / OpenAI-compatible / Gemini), **Base
3940
- URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display only), and Balance
3941
- (DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
3942
- `keys.<ENV NAME> = { name, shape, baseUrl, priceInPerM?, priceOutPerM? }`. The last two columns are the customer's own price
3943
- (**In $/1M**, **Out $/1M**, plain boxes; blank = not set, a number >= 0 otherwise; the server refuses a half-set pair with a plain
3944
- error and saves nothing, and the page saves NOTHING in a row with exactly one price box filled, showing "Fill both price boxes
3945
- or clear both." when the person leaves that box, until both boxes are filled or both cleared).
3946
- **Open keys folder** (`POST /api/settings/open-keys-folder`) opens `~/.config/bareloop` in the file manager (`xdg-open` / `open`,
3947
- argv array, never the file itself); where it cannot, the page shows the path as text. Defaults for a key with no saved entry:
3948
- `ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
3949
- OpenAI-compatible / `https://api.deepseek.com/v1`; `OPENAI_API_KEY` OpenAI-compatible;
3950
- `GEMINI_API_KEY` Gemini; `LOCAL_API_KEY` OpenAI-compatible / `http://127.0.0.1:11434/v1` (Ollama, LM
3951
- Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **The
3952
- only price shown is the customer's own, typed in the two price boxes** (cost recording, the estimated provenance, spend sums, caps
3953
- and the monthly limit are unchanged; a local model has no known rate, so its rounds come out as a
3954
- non-zero loud guess, never a real $0).
3955
-
3956
- `providerrows.js` is the ONE owner of "model → key + shape + URL": `keyRows`, `findRow` (rows on the
3957
- same shape + endpoint; the Name breaks a tie), `modelChoiceFor` / `chatModels` (Chat's Model menu IS
3958
- the rows; a blank Name is not offered), `keyNameFor` / `applyConfiguredKey` (a job on `(provider,
3959
- baseUrl[, model])` reads the matching row's variable, else the provider's built-in one). All doors
3960
- (`run-u`, `interview`, `author`, the panel's drafting) use it. Routes (all behind the per-start token
3961
- and Origin check; the chat can never reach them): `GET /api/settings/providers`, `POST .../row`
3962
- (`{envName, name, shape, baseUrl}`), `POST .../balance-note`, `POST .../test` (one
3963
- `checkProviderReachable` models-list GET at the row's shape + URL, $0; a refused connection reads
3964
- `nothing answering at <URL>`), `GET .../balance?env=`
3965
- (DeepSeek-hosted row, server-side), and `GET /api/author/models` (Chat's Model menu). The old
3966
- `providers.<row>.key` config field and the key dropdown are gone. Like the authoring routes, a POST
3967
- body over 1 MiB is refused `413`.
3920
+ **Settings > Providers (P4b, P8 — rewritten from fwdloop's m4d-am3 layout; `src/providerrows.js`, `src/modellist.js`, `src/panel/settingsroutes.js`).** One row per key in `.env` that HAS A VALUE, plus a key
3921
+ only the shell holds when config already names it (`keys.<ENV>` or `models.<ENV>`; its status reads `found in shell`) — nothing is hardcoded, there is no add-row and no remove-row control. A missing `.env`
3922
+ is created (mode 600, dir 700) on the first Providers read with two comment lines and no key. A value of `null` counts as SET and means "no real key" (the provider is handed the placeholder
3923
+ `no-key-needed`, never the word `null`; the row shows `set in file`). A key whose value has a space or a line break shows `The key has a space or a line break in it. Fix it in your keys file, then
3924
+ reload keys.` Each row: the env NAME + status, an **API shape** select (Anthropic / OpenAI-compatible / Gemini), the typed **Base URL** (empty = no run, no list, `type the Base URL`), the **Old max_tokens**
3925
+ tick (applies to `openai-api` only), **Tokens** (the key's spend), `[ Test ] [ ⟳ Refresh ]`, and its model lines: a **Use** tick, the model name, **In / Out $/1M** boxes, tokens (`—` when no spend row names the model).
3926
+ A listed id from Refresh is shown unticked and is not saved until touched; `+ type a model name` adds a line unticked. Saves are on change; success reads `saved — applies from the next model call`;
3927
+ every refusal is a plain sentence in `say` (`Fill both prices to tick this model.`, `Fill both price boxes or clear both.`, `A price must be a number, 0 or more.`, …). **Open keys folder** (in the KEYS strip,
3928
+ `POST /api/settings/open-keys-folder`) opens the folder of the keys file only (`xdg-open` / `open`, argv array, detached, never a path from the request; a non-empty body is `no-path-allowed`); where it cannot —
3929
+ another OS, a failed spawn, a test process with no injected opener — it answers `opened:false` and the page shows the path as text.
3930
+
3931
+ `providerrows.js` is the ONE owner of "model → key + shape + URL": `keyRows` (`{ envName, provider, baseUrl, legacyMaxTokens, models }`), `effectiveModels` (the older single-name read), `tickedLines`,
3932
+ `findRow` (rows on the same shape + endpoint — a blank Base URL is blank, there is no default host; the row with a ticked, priced line for the model breaks a tie), `modelChoiceFor(rows, { provider, model })`
3933
+ (by the KEY's env name; the line must be ticked and priced), `judgePick` (the three refusals: `Tick a model in Settings > Providers. Nothing was spent.` / `Pick a model. Nothing was spent.` /
3934
+ `<KEY> is not set up: put its key in your keys file and type its Base URL in Settings > Providers. Nothing was spent.`), `resolveWorker` (a run's line: the spec's model, else `--model`; `--provider <KEY>`
3935
+ narrows), `keyValueOf` (a row's own variable; the `null` marker becomes the placeholder). All doors (`run-u`, `run <bundle>`, `interview`, `author`, the panel's drafting) use it. Routes (all behind the per-start
3936
+ token and Origin check, GET too; the chat can never reach them): `GET /api/settings/providers` (`{ok, keysFile:{path,exists,refusal}, configProblem, shapes, rows:[{slot, label, envVar, keyStatus, shape, baseUrl,
3937
+ legacyMaxTokens, tokens, models}]}`), `POST .../provider` (`{slot, shape?, baseUrl?, legacyMaxTokens?}`, every field judged before anything is written), `POST .../model` (`{slot, model, use?, inPerM?, outPerM?}`),
3938
+ `POST .../models` (Refresh) and `POST .../test` (both ONE free GET of `<Base URL>/models` through `src/modellist.js`, key only in a header per shape, redirect not followed, 4 s; no completion, no spend row),
3939
+ and `GET /api/author/models` (Chat's Model menu: every ticked-and-priced line, `{slot, model, label, thinking}`, `<KEY> · <model>`). There is no Balance route. Like the authoring routes, a POST body over 1 MiB is refused `413`.
3940
+ A Chat card carries `provider` (the key's env name) beside `model`; `/api/author/model-check`, `start-from` and `reuse-jobs` take `?provider=` too.
3968
3941
 
3969
3942
  ### The source front door — a plain folder/file/URL, no `--patient` (PRD item 33/M2, `src/source.js`)
3970
3943
 
@@ -4360,4 +4333,4 @@ Goal, Success, Guardrails and Judge examples are one box, **The job**, and Sourc
4360
4333
  - **The job:** each plain line is numbered 1, 2, 3 (the number sits in a gutter outside the box). A `~` line has no number and belongs to the numbered line above; consecutive `~` lines join into ONE rule with `'; '`. A `~` line starting `PASS:` / `FAIL:` is a judge example (a rubric check needs at least one of each; a deterministic check refuses them); any other `~` line is a guardrail. A `~` before line 1 is a $0 red. Empty lines are ignored. `parseJobBlock(text)` → `{ ok, lines: [{ n, text, rule, examples }] }` or `{ ok:false, error }`; `jobBlockFitsCheck(lines, rubric)` is the Check-type cross-check.
4361
4334
  - **Inputs:** one `label: value` per line, numbered. Line 1 = the repo (absolute path). Lines 2+ = files or folders inside that repo, repo-relative; `proveInputFiles(inputs)` (`src/source.js`) proves them at $0 before any token — exists, inside the repo, tracked, and (files) not an environment file by name, not a symlink, text, under the size ceiling, a known secret shape in a file's content is MASKED wherever bareloop records it, never refused (the P6 worktree door ruling; `.env` by name and symlinks stay refused). A miss is `Inputs, line N: ...`. Search lines and files outside the repo are `only files inside the repo for now`. The panel proves them in `POST /api/author/start` (400 naming the line); the CLI re-asks.
4362
4335
  - **Drafter:** every stage carries `fromLine`, or the line is listed in `closeDecl.refused` with a reason (see `closeDecl` above); the confirm turn's checks are `{ text, fromLine: [N, ...] }`; the plan draws a multi-line check under EACH line it serves.
4363
- - **Panel:** the card is `{ jobName, checkType, model, jobText, inputs, destination, capUsd, maxWallMs }`. The confirm plan is a structured chat message `{ role: 'bot', kind: 'plan', plan }` and the Job tab reads the SIGNED spec through the same model (`src/panel/jobplan.js`: `planFromConfirm`, `planFromSpec`); the page draws both with one `renderJobPlan()`. `GET /api/runs/:id/job` returns `plan`, `jobLines`, `inputs`, `specHash`, `signedAt` (the run's FIRST `job-start`) and `signed` (`YYYY-MM-DD HH:MM · <first 6 of specHash>`, local time). An older spec (no `jobLines`) renders the signed goal as line 1 with its stage names as checks and the note `older job — numbered lines were not saved`; Reuse / Edit / Resume prefill the same fallback.
4336
+ - **Panel:** the card is `{ jobName, checkType, provider, model, thinking, jobText, inputs, destination, capUsd, maxWallMs }` (`provider` = the picked key's env name, `thinking` = the "Thinking on" tick). The confirm plan is a structured chat message `{ role: 'bot', kind: 'plan', plan }` and the Job tab reads the SIGNED spec through the same model (`src/panel/jobplan.js`: `planFromConfirm`, `planFromSpec`); the page draws both with one `renderJobPlan()`. `GET /api/runs/:id/job` returns `plan`, `jobLines`, `inputs`, `specHash`, `signedAt` (the run's FIRST `job-start`) and `signed` (`YYYY-MM-DD HH:MM · <first 6 of specHash>`, local time). An older spec (no `jobLines`) renders the signed goal as line 1 with its stage names as checks and the note `older job — numbered lines were not saved`; Reuse / Edit / Resume prefill the same fallback.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",
@@ -27,7 +27,7 @@
27
27
  "check:lockfile": "node scripts/check-lockfile.mjs"
28
28
  },
29
29
  "dependencies": {
30
- "bare-agent": "^0.51.0",
30
+ "bare-agent": "^0.52.0",
31
31
  "bareguard": "^0.21.0",
32
32
  "litectx": "^0.32.0"
33
33
  },
package/src/authorflow.js CHANGED
@@ -73,7 +73,7 @@
73
73
  // No spine emission here — M4 wires events.
74
74
 
75
75
  import { renderInputs } from './jobblock.js';
76
- import { draftCallOptions } from './providers.js';
76
+ import { thinkingCallOptions } from './thinking.js';
77
77
  import { createRequire } from 'node:module';
78
78
  import { realpathSync } from 'node:fs';
79
79
  import { join, relative, isAbsolute } from 'node:path';
@@ -1536,7 +1536,7 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
1536
1536
  */
1537
1537
  /**
1538
1538
  * @param {{ceilingUsd?: number|null,
1539
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void}} [o]
1539
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void}} [o]
1540
1540
  * `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
1541
1541
  * `onCall` is a REPORTING seam fired as each metered call lands — the shell's
1542
1542
  * only chance to know what has been spent while the run is still alive. It is
@@ -1557,9 +1557,9 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
1557
1557
  const call = { label, ...priceOf(r) };
1558
1558
  entries.push(call);
1559
1559
  raws.push(scrubRaw({ label, attempt, text: r?.text, reason: r?.error ? String(r.error) : null }));
1560
- // bare-agent's per-run `thinkingApplied` (true | false | null) rides the REPORTING seam only — never the stored entry,
1561
- // so the cost report and every persisted shape stay as they were; absent when thinkingMode was not sent
1562
- onCall({ ...call, ...(r?.thinkingApplied === undefined ? {} : { thinkingApplied: r.thinkingApplied }) });
1560
+ // bare-agent's per-run `thinkingStatus` ('applied' | 'unsupported' | 'ignored' | 'unknown') rides the REPORTING seam only —
1561
+ // never the stored entry, so the cost report and every persisted shape stay as they were; absent when thinkingMode was not sent
1562
+ onCall({ ...call, ...(r?.thinkingStatus === undefined ? {} : { thinkingStatus: r.thinkingStatus }) });
1563
1563
  return entries.at(-1);
1564
1564
  },
1565
1565
  /** @param {{label: string, costUsd: number|null, unpricedRounds: number}[]} calls
@@ -1599,13 +1599,13 @@ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
1599
1599
  * `cacheMessages` (F18) and `maxTokens` (F30) are not optional on this stack.
1600
1600
  * `rates` is the customer's own price for this provider's key row (USD per 1K, `ratesFor`);
1601
1601
  * absent = bare-agent's built-in guess, exactly as before.
1602
- * `draftThinking` ('on' | 'off', operator knob; default 'off') adds the drafter's per-call thinking option via `draftCallOptions`.
1602
+ * `draftThinking` ('on' | 'off', the job's "Thinking on" tick spelled as a word; default 'off') adds the drafter's per-call thinking option via `thinkingCallOptions` (src/thinking.js, the one owner).
1603
1603
  * @param {any} provider @param {{system?: string, maxTokens?: number, rates?: {in: number, out: number}|null, draftThinking?: string}} [o]
1604
1604
  * @returns {(messages: any[], tools: any[], opts?: any) => Promise<any>}
1605
1605
  */
1606
1606
  export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens = AUTHOR_MAX_TOKENS, rates = null, draftThinking = 'off' } = {}) {
1607
- // the drafter's per-call options (src/providers.js `draftCallOptions`, one owner); a bad mode throws HERE, at construction
1608
- const callOptions = draftCallOptions(provider, draftThinking);
1607
+ // the drafter's per-call options (src/thinking.js `thinkingCallOptions`, one owner); a bad mode throws HERE, at construction
1608
+ const callOptions = thinkingCallOptions(provider, draftThinking);
1609
1609
  return async (/** @type {any[]} */ messages, /** @type {any[]} */ tools, /** @type {any} */ opts = {}) => {
1610
1610
  const loop = new Loop({ provider, system, ...(rates ? { rates } : {}) });
1611
1611
  const wired = tools.map((t) => ({
@@ -2265,7 +2265,7 @@ export async function runConfirmTurn({
2265
2265
  * generate: Function, seedReadFn?: Function, closeCtx?: any,
2266
2266
  * ceilingUsd?: number|null,
2267
2267
  * onPhase?: (phase: string, data?: any) => void,
2268
- * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingApplied?: boolean|null}) => void,
2268
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
2269
2269
  * maxRevisions?: number, structureRetries?: number,
2270
2270
  * structuredMode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
2271
2271
  * priorCalls?: {label: string, costUsd: number|null, unpricedRounds: number}[]|null,