bareloop 0.40.0 → 0.42.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 (77) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +141 -128
  4. package/package.json +5 -4
  5. package/src/authorflow.js +346 -38
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +82 -20
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +39 -30
  10. package/src/authorscout.js +112 -4
  11. package/src/bundle.js +7 -4
  12. package/src/bundlerun.js +4 -1
  13. package/src/cli.js +1 -1
  14. package/src/config.js +129 -29
  15. package/src/declaredclose.js +23 -12
  16. package/src/draftspend.js +4 -4
  17. package/src/facts.js +317 -0
  18. package/src/factwords.js +129 -0
  19. package/src/index.js +2 -4
  20. package/src/inputlook.js +301 -0
  21. package/src/interviewrun.js +17 -25
  22. package/src/job.js +15 -8
  23. package/src/jobblock.js +7 -2
  24. package/src/judged.js +11 -43
  25. package/src/keysfile.js +15 -7
  26. package/src/kinds.js +86 -5
  27. package/src/labelscan.js +170 -0
  28. package/src/modellist.js +84 -0
  29. package/src/monthly.js +23 -43
  30. package/src/pagefetch.js +125 -0
  31. package/src/panel/authorroutes.js +34 -16
  32. package/src/panel/authorsession.js +161 -80
  33. package/src/panel/index.html +553 -152
  34. package/src/panel/jobplan.js +34 -11
  35. package/src/panel/runroutes.js +3 -0
  36. package/src/panel/server.js +54 -18
  37. package/src/panel/settingsroutes.js +232 -173
  38. package/src/planrun.js +5 -4
  39. package/src/providerrows.js +226 -141
  40. package/src/providers.js +58 -332
  41. package/src/providershapes.js +18 -0
  42. package/src/replay.js +1 -1
  43. package/src/rubricstep.js +804 -0
  44. package/src/source.js +166 -1
  45. package/src/thinking.js +130 -0
  46. package/src/u-readout.js +1 -1
  47. package/src/userrun.js +64 -75
  48. package/types/authorflow.d.ts +272 -18
  49. package/types/authoring.d.ts +29 -1
  50. package/types/authorjob.d.ts +9 -7
  51. package/types/authorreadout.d.ts +27 -2
  52. package/types/authorscout.d.ts +72 -2
  53. package/types/config.d.ts +23 -6
  54. package/types/declaredclose.d.ts +4 -2
  55. package/types/draftspend.d.ts +8 -8
  56. package/types/facts.d.ts +71 -0
  57. package/types/factwords.d.ts +42 -0
  58. package/types/index.d.ts +1 -1
  59. package/types/inputlook.d.ts +238 -0
  60. package/types/job.d.ts +5 -5
  61. package/types/jobblock.d.ts +8 -1
  62. package/types/judged.d.ts +5 -36
  63. package/types/keysfile.d.ts +15 -4
  64. package/types/labelscan.d.ts +38 -0
  65. package/types/modellist.d.ts +50 -0
  66. package/types/monthly.d.ts +4 -3
  67. package/types/pagefetch.d.ts +71 -0
  68. package/types/panel/authorsession.d.ts +20 -11
  69. package/types/panel/jobplan.d.ts +12 -4
  70. package/types/panel/server.d.ts +18 -6
  71. package/types/providerrows.d.ts +192 -99
  72. package/types/providers.d.ts +34 -159
  73. package/types/providershapes.d.ts +10 -0
  74. package/types/rubricstep.d.ts +286 -0
  75. package/types/source.d.ts +94 -0
  76. package/types/thinking.d.ts +90 -0
  77. package/types/userrun.d.ts +17 -1
@@ -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
@@ -721,7 +721,7 @@ attempt PLUS its close. Unpriced is never free: a null cost is a `pricing-red` s
721
721
  never retried, and the ladder is otherwise ONE retry (`JUDGE_ATTEMPTS`) — after that a broken
722
722
  judge is an instrument stop, never a red about the tree.
723
723
 
724
- **The rulebook, and what each rule can raise** (`JUDGE_RULES` in `src/judged.js`; every rule carries a plain-words `means` the case-proposal prompt renders, so a proposed practice case never expects a red the rule cannot raise). The judge only POINTS (quotes lines verbatim); code does every check; unsure is red; the judge never says pass/fail. Quotes are matched by bareguard's `quoteIn(..., {wholeLines:true})` (bareguard `^0.21.0`): a contiguous run of whole artifact lines, so a bare fragment cannot pass by hiding in a longer line, while a dropped leading `* ` of a JSDoc tag line still matches. `has-doc` checks only that a JSDoc block sits directly above the function (read off the artifact by `docBlockAbove`; the judge's quote is a pointer that must lie inside that block), so a documented function never breaks it. **`says-what`** (the doc's description must say more than the function's own name) reds when the described prose, minus a fixed list of function words and the name's own words (camel/snake/kebab, light inflections, and a prefix echo such as `init`/`initialize` when the shorter word is at least 4 letters), leaves nothing; no description, or an unlocatable quote, reds. It catches a pure name echo only ("Returns the total." for `getTotal` passes), and with NO doc block at all it stays silent and leaves the red to `has-doc` (the card is not required to carry both). `params` reds a declared parameter with no tag, an `@param` naming no parameter (each destructured slot absorbs one unmatched root tag), and, when the doc names any field of a simple destructured parameter, a missing field; `@param options` alone for `{ x, y }` passes. `returns` reds a returning function with no `@returns`. A practice case that expects a `has-doc` red on a function that has a block is refused as a legality red before any call (`validateCalibrationSet`).
724
+ **The rulebook, and what each rule can raise** (`JUDGE_RULES` in `src/judged.js`; every rule carries a plain-words `means` the case-proposal prompt renders, so a proposed practice case never expects a red the rule cannot raise). The judge only POINTS (quotes lines verbatim); code does every check; unsure is red; the judge never says pass/fail. Quotes are matched by bareguard's `quoteIn(..., {wholeLines:true})` (`wholeLines` arrived in bareguard 0.21; bareloop now needs `^0.24.0`): a contiguous run of whole artifact lines, so a bare fragment cannot pass by hiding in a longer line, while a dropped leading `* ` of a JSDoc tag line still matches. `has-doc` checks only that a JSDoc block sits directly above the function (read off the artifact by `docBlockAbove`; the judge's quote is a pointer that must lie inside that block), so a documented function never breaks it. **`says-what`** (the doc's description must say more than the function's own name) reds when the described prose, minus a fixed list of function words and the name's own words (camel/snake/kebab, light inflections, and a prefix echo such as `init`/`initialize` when the shorter word is at least 4 letters), leaves nothing; no description, or an unlocatable quote, reds. It catches a pure name echo only ("Returns the total." for `getTotal` passes), and with NO doc block at all it stays silent and leaves the red to `has-doc` (the card is not required to carry both). `params` reds a declared parameter with no tag, an `@param` naming no parameter (each destructured slot absorbs one unmatched root tag), and, when the doc names any field of a simple destructured parameter, a missing field; `@param options` alone for `{ x, y }` passes. `returns` reds a returning function with no `@returns`. A practice case that expects a `has-doc` red on a function that has a block is refused as a legality red before any call (`validateCalibrationSet`).
725
725
 
726
726
  **A judged stage may only judge files inside the job's write fence** (`judgedOutsideFence`, `src/authoring.js`, called from `validateDeclaration`, which every door reaches: the authoring revise loop, `validateCloseDecl` at signing and runner pre-flight, and `validateJob` for hand-written or reused specs). A path outside `writeScope` reds `judged-outside-write-scope` at draft time, because a finding on a file the run is fenced out of can never be fixed. With no fence to compare against (the plain-folder deferral) the rule is not run.
727
727
 
@@ -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
@@ -3670,7 +3668,7 @@ key you want first (the panel picks the row by the Model menu).
3670
3668
  **The progress list (2026-10-04)** — `GET /api/author/:id` state carries `steps: [{id, label, status:
3671
3669
  'running'|'done'|'failed', detail}]`, one entry per pipeline step in first-seen order, updated in place (ids/labels:
3672
3670
  `STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A drafting call after the first
3673
- relabels the ONE drafting line `drafting (retry N of M)` (same line, never a second one). A refusal sets
3671
+ relabels the ONE drafting line `drafting (retry N of M)` after a refused try, `drafting (look N of M)` after a passed one (same line, never a second one). A refusal sets
3674
3672
  `state.error` and fails exactly one step with the same text as its `detail`; pipeline text is never a chat message
3675
3673
  (`messages` holds person/model turns only). Two last steps, `hash` ("generating hash", detail `spec hash <full hash>`, done at sign-prepare) and `signed` ("signed hash", pushed by `signRun` when the sign is accepted); the old "SIGNING PREPARED" / "signed — … run starting detached" thread bubbles are gone. The page draws each step's `detail` on its own `> ` line under the step. Destination follows the Source: for a repo source it is the write
3676
3674
  fence (relative `writeScope` globs, never proven as a directory); for a folder source an absolute directory. Both
@@ -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
 
@@ -4346,7 +4319,7 @@ blast radius.
4346
4319
 
4347
4320
  ## Constraints
4348
4321
 
4349
- - **Node >= 20** (bareguard's floor governs the suite).
4322
+ - **Node >= 22** (`engines.node`; barebrowse, a production dependency since Part 1c, needs it).
4350
4323
  - **Pure ESM + JSDoc**; generated `.d.ts` ship, never hand-written (LIBRARY_CONVENTIONS §2).
4351
4324
  - **Secrets load from the environment** and never enter the spine, configs, or ledger —
4352
4325
  an append-only record that captures a key captures it forever.
@@ -4357,7 +4330,47 @@ blast radius.
4357
4330
 
4358
4331
  Goal, Success, Guardrails and Judge examples are one box, **The job**, and Source is **Inputs** (CLI interview and panel card alike).
4359
4332
 
4360
- - **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.
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 that holds a judged part needs at least one of each; a rubric on a local file does not; 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, { judged? })` 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.
4337
+
4338
+ ### A rubric job on your own local files — no judge yet (Part 1a, `src/rubricstep.js`, `docs/product/ITEM33-BUILD.md` "Addendum 2026-10-12")
4339
+
4340
+ A **Rubric** job whose first Input is the full path of a FILE (not a repo folder) works on your local text files; there is no repo, no language and no scout. Each Inputs line is `label: /absolute/file`.
4341
+
4342
+ - **Door.** `proveRubricInputs({ inputs, into, destination? })` (`src/source.js`) proves every line at $0 and freezes the files under `input/` of the same hidden-git tree a plain folder gets; `source.json` also records `inputs: [{ label, rel, sha256 }]` (hashed once, there). Refused in plain words naming the line and ending "Nothing spent.": a web address or a `search` line ("line N: web inputs (URLs, search) arrive in a later build. Nothing spent."), a relative path, a missing file, a folder, a symlink, an environment file, a file over 5 MiB (`RUBRIC_INPUT_MAX_BYTES`), text that is not valid UTF-8, a repeated label, a known secret shape. `isRubricFilesManifest(manifest)` tells this manifest from a repo's. Destination is the full path of a folder (the finished files are copied there on a minted green); a relative one is refused at $0.
4343
+ - **No examples needed.** `jobBlockFitsCheck(lines, rubric, { judged })`: the `~ PASS:` / `~ FAIL:` lines are required of a rubric job ONLY when it holds a judged part. A rubric on a file holds none in this build, so a job without examples is fine (written examples are kept and unused until the judge arrives). A rubric on a repo still needs them.
4344
+ - **One output per job line.** The worker writes `output/steps/<n>.json` for job line n: `{ "text": "...", "cites": { "<input label>": [ { "claim": "...", "quote": "..." } ] } }`. The worker's goal is written by code (`rubricGoal`) from the numbered lines and the frozen input paths, and states this contract (`stepFileContract`, one text shared with the composer's prompt). `output/steps/` is delivered with the rest of `output/` (`copyOut`: `steps/<n>-YYYY-MM-DD.json` under Destination).
4345
+ - **The close.** The text genre (`closeDecl.lang: "text"`, `RUBRIC_LANG`; `DECLARABLE_LANGUAGES` = the code languages plus `text`) has two kinds: the mandatory `changed-from-seed` guard and `rubric-step`, one stage per job line (`params: { step: n, parts: [...] }`, `fromLine: [n]`). A code job never sees `rubric-step` (`catalogueForLang`). `rubric-step` builds bareguard's rubric at every run (input shas from `source.json`) and grades the step file: `happened` (it exists and is a JSON object), `text` present, then the countable limits on `text` (`maxWords`, `minWords`, `maxLines`, `sections`, `sectionWords`) and `cited` per input the step cites (every quote found word for word in that input, spacing and `**` ignored; every number in a claim in its quote). A missing, cut-off or non-object file is a RED; an input that no longer matches its frozen sha, an unreadable `source.json`, a file over 5 MiB or a rubric that cannot be built is an INSTRUMENT STOP (exit 97). A step file cut inside valid JSON cannot be told from a short answer; that limit is stated, not hidden. A symlinked step file is red, never followed.
4346
+ - **Every part is sorted at drafting** (`class`: `countable` | `judged` | `guard` | `none`), and `checkRubricParts` reds in the person's words: `line N: this rule needs the judge, which arrives in the next build. Nothing spent.`; `line N: '<part>' can't be counted or checked against a quote. Say it as a count (words, sections, rows) or as something a quote must show.`; a guard (`line N: '<part>' is about web sources, and this job has no web input. Nothing spent.`); contradictions name both parts and both numbers; a stage must serve exactly its own line; a `cited` part must name one of the job's input labels. These codes are `rubric-*` and print whole in the drafting and signing stops (`draftStopText`, `signingStopText`).
4347
+ - **Signing is $0.** `prepareSigning` runs its paid calibration gate only for a close with a judged stage; a rubric-step close has none, so signing makes zero judge-round records. Not in this build: the judge, the final human accept, web inputs and search, chips (see the addendum's deferred list).
4348
+ - **Confirm turn (ruling 1A).** A rubric job on local files runs the same form -> confirm turn -> sign flow a repo job has (one confirm turn, two rounds at most; the scout stays off, the survey is the numbered inputs). `authorCloseForJob` runs it when `isRepo` or `lang === 'text'`. The protections shown are code's, never the model's: `confirmProtections({ verdictType, lang, writeScope, jobLines })` lists the text genre's guard (`changed-from-seed`), the write fence and, for every numbered line, `line N file — output/steps/N.json must exist, be complete JSON, and carry text`; the same list is fed to the model as ALREADY COVERED, and `RUBRIC_CONFIRM_NOTE` tells it a check here is a counted limit or a claim backed by a quote (anything needing judgement goes in `notChecked`). The confirmed plan reaches the composer, and its goal sentence rides in the worker goal as "The plan the person confirmed". `POST /api/author/start` skips the repo proof of Inputs for a rubric card whose first input is a local file (`looksLikeLocalFile`); the session proves each line.
4349
+ - **Fix-loop number.** A red `rubric-step` stage puts `detail.redCount` on its result: failing claims under a `cited` check (bareguard's `measured`) plus 1 per other failing check; `trendValueOf` hands it to the governor as `closeGrade` `{ stage, value }`, lower is better (no prose is parsed).
4350
+ - **Panel wording.** `stageQuestionText` words a rubric-step stage "line N — does its output pass the line's checks?". The worker brief also tells the worker to copy every number, time and price in a claim exactly as the input writes it ("9:27 AM", not "09:27").
4351
+ - **Shown == run (run mv5bk400).** The plan card after drafting is built from the signed close (`checksOfClose`, code-written sentences), not from the confirm turn's prose. A countable part may list the confirmed checks it carries out in `covers` (the composer prompt numbers them `#n`); `reconcileRubricPlan` moves every confirmed check no part covers to Not checked ("... — nothing in this close counts it or checks it against a quote"). `signRun` calls `planMatchesClose(session.state.finalPlan.checkItems, closeDecl)` and refuses, spawning nothing, when the two differ. `minClaims` / `maxClaims` (parts with `value` + `source`) count the claims listed under an input: `flattenStep` takes the count from the file's own `cites.<label>` list as the field `claims:<label>` and bareguard's `min`/`max` compares it. A free-text note is never a check.
4352
+ - **Labels.** A text close carries `genre: "TEXT"` (`genreForLang`; `TYPES` is for code languages, and a genre must match its language); its spine records carry no `judgedCount` (no judge ran); the progress list words a file job's steps from `FILE_JOB_STEP_LABELS` ("copying input files", "reading input files").
4353
+ - **Panel topbar.** `bareloop · v<version> · automate a job, verified`: the version is bareloop's own package.json, templated into the page at serve time (`__BARELOOP_PANEL_VERSION__`).
4354
+ - **After drafting, the code-worded card is the only plan on screen (run mv5e1ktb).** The confirm turn's plan bubble is the drafter's own prose (written before any close existed) and used to stay in the thread above the code-worded card; when the rubric card is ready the earlier `kind:'plan'` messages are dropped. `partSentence` now reads "1 line" / "1 word" for a count of one.
4355
+ - **Each `~` guardrail is its own row on the plan card.** `planFromSpec` / `planFromConfirm` lines carry `rules: string[]` (the line's own `rules`, else the joined `rule` split on "; "); `renderJobPlan` draws one `~ rule` row per rule under its numbered line, in the plan card and the Job tab alike (one renderer). Gutter numbers stay on job lines only.
4356
+ - **The review door's held line is true for a no-judge close.** Credit is held by VERDICT CLASS (`quarantinesCredit`: soft-green), whether or not a judge ran; the door now says "this is a rubric (soft-green) job, so its green earns NO reuse and NO learning credit until you accept it, whether or not a judge ran". Wording only; `bridges.js`/`reuse.js`/the Reuse refusal still say "rendered by the judged floor".
4357
+ - **A refused drafting try records why.** `authorClose` emits `onPhase('author-try-failed', { call, i, of, reds | reason, declaration? })` for a try refused by validation (the scrubbed reds), by a vacuous or stopped seed read (a `reason`), or by an all-cut-off reply; the panel writes `{kind:'try-refused', no, call, reason, reds?, declaration?}` to `draft-log.jsonl` (`tryRefusedText`, `src/authorreadout.js`, the code's own red details) and shows `<call>: refused — <reason>` on the drafting step's detail line. The CLI prints it via `phaseLine`. The refused declaration itself rides beside it (`declaration`, secrets masked; `declarationText` + `declarationCut` when over 60000 characters), so a failed draft can be read and replayed at $0 (`declarationRecord`).
4358
+
4359
+ ### Required facts — a line that finds, lists or looks something up (Part 1b, `src/facts.js`, `docs/product/ITEM33-BUILD.md` "Addendum 2026-10-13" and "Part 1c", PRD item 33 RF1–RF22)
4360
+
4361
+ Check what the person asked for, not what the worker chose to write. bareloop needs bareguard `^0.24.0` (`cited` + `decides`, bind-only and `text` decides, word-edge anchors; 0.24 lets money currency/unit be lists, ruling #65, but bareloop facts still carry one form).
4362
+
4363
+ - **The part.** A `rubric-step` stage's `params.parts` may hold ONE part of `class: "fact"` (a line that finds, lists or looks up): `{ text, class: "fact", source: <input label>, facts: [{ name, shape, unit? | currency?, phrase?, anchor?, until?, rule?, aiProposal? }], count?: { min?, max? }, rules?: [n], covers?: [n] }`. `shape` is `money | number | date | time | phrase | text`; `rule` is `{ op: lt | lte | gt | gte, value }` or `{ op: "between", lo, hi }` (not on `text` or `phrase`); `money` names exactly one of `unit` (the input's own words, e.g. `"US dollars"`) or `currency` (`$ € £`); `phrase` carries the fixed `phrase` the quote must contain; `anchor` / `until` are labels copied VERBATIM from the input. A fact name is letters, spaces, `_` or `-` only (it is written into the claim text bareguard scans for numbers). A line with a fact part has no word, section or claim-count parts (a drafting red). Code fills `tested` (the early-test evidence) and `sample` (what the drafter saw of a big input) into the signed part; a model-written one is replaced.
4364
+ - **The step file.** `output/steps/<n>.json` of a facts line is `{ "results": [ { "quote": "...", "values": { "<fact name>": "<value as the quote writes it>" } } ] }` — no free text, no `cites`. ONE quote per result holds all its facts, so a worker cannot mix a price from one result with a time from another. `buildStepSpec` makes one bareguard `cited` check with `decides` (the fact's index is its `d<j>` key; a phrase fact is a presence decide and takes no value). The claim text is written by CODE from the values (`name: value; ...`); a `count` becomes `min` / `max` on the number of results. Every value is compared by shape and exactly (`8:15 AM` equals `08:15`, `389` equals `389.00`); a red names the result and the fact in plain words (`plainFactItem`). A JSON number in `values` is read as the string it prints as.
4365
+ - **No record cutting (Part 1c).** A fact part carries no `record`, `tested` or `sample` (a `record` field is an unknown-field red). The worker's brief says "one quote per result, copied from that result's own text" (bareguard's quote cap of 4000 characters stays its own). Drafting does not grade: it only proves each fact CAN be checked (below).
4366
+ - **Repeat rule (Part 1c M1, M4).** On a fact line asking for N results (`count.min`), `checkFactWords` (`src/factwords.js`, the one owner) refuses at $0 an anchor label the label list counts fewer than N times (`rubric-fact-label-rare`) and a `phrase` fact whose words `countPhrase` finds in the input fewer than N times (`rubric-fact-phrase-rare`); count 1 or none keeps a once-only one legal. `RUBRIC_RULES` states both.
4367
+ - **The look, then the draft (Part 1c).** For a rubric job on files, `authorCloseForJob` runs `runInputLook` (`src/authorscout.js`; pure parts in `src/inputlook.js`) on the frozen input text BEFORE the confirm turn and the draft, when a `provider` is present (`lookFn: null` = no look; a panel test that replaces `generate` gets none unless it passes `lookFn`). Read-only by construction: tools `search_input` / `read_input` over in-memory text, `LOOK_ROUNDS` 8, a reserved final round whose only tool is the strict answer form `report_look`, spend metered into the drafting book (`calls` absorbed like the repo scout's). Its report (facts with a label picked from the list, prose lines, not-found entries) is verified by code (`verifyLookReport`: a label off the list, an unknown shape/line/input, or a phrase not in the source is moved to not-found, named) and rendered into the drafter's prompt (`renderLookReport`); a failed or reportless look is an ABSENT survey and stops drafting at $0 (`scout-absent`, "the first look at your repo or input did not complete"). Every place, date or name in the job line is a fact too, bound to the label before it (a direction = phrase facts `Leaves <place>` / `arrives at <place>`); prose lines and their `~` limits are not looked up.
4368
+ - **Labels are picked, never typed (`src/labelscan.js`).** `scanLabels(text)` lists, per shape (money, number, date, time, text), the words right before each value with counts (top 40 per shape; one generic scan, no per-site table; a page reader's `[ref=N]` tags are ignored). `labelOnList(scan, shape, idOrText)` is the one test (id or exact words, white-space blind). The look and the drafter pick labels only from the list shown in the prompt (`renderLabelList`). Fact-line reds are plain sentences; the confirm step and the drafter share one rule text (`RUBRIC_RULES`), and the drafter's per-shape field guide is generated from bareguard's `rubricVocabulary`.
4369
+ - **Found-once check (`src/factwords.js`, `checkDeclarationWords`, run after validation in `authorClose`).** Every anchor must be on the input's label list for its shape, and every `until`, phrase and unit must be in the source once (white-space blind: U+202F, U+00A0; phrases whole-word, case-blind). A miss is a red naming the word and the input: `rubric-fact-label-off-list`, `rubric-fact-word-missing`, `rubric-fact-unread` (an input that cannot be read is a red, never a skipped check). Nothing is graded; finding and grading are the run's and the close's job.
4370
+ - **No silently dropped rule (RF16).** When a close holds a fact part or any part with `rules`, every `~` rule of every job line must be carried out by a part (its number in the part's `rules`, 1 = the first `~` rule) or listed in `refused` as `{ line, rule, reason }`, or its whole line refused; a rule that is none is a $0 red (`rubric-rule-dropped`). `refused` entries may now carry `rule`. A number the person did not write (a bound not found in the line's own words) must be marked `aiProposal: true` (the card says "AI's proposal — check it"), and an AI-proposed time limit must be `between`, never one-sided (RF17).
4371
+ - **The prose step.** A line that writes keeps its word / section / line counts. When any fact line comes BEFORE it, code puts one `numbersFromFacts` part on it (`withFactBindings`; the model never writes it, and a missing or wrong one is a drafting red): every number in `text` must appear in a quote of those fact lines' step files (bareguard's `numbersInQuote`: token match, not canonical, so `v0.41.0` is one token). The card says, under Not checked, `line N: what the text says — you judge it at Accept`. At the close, a tied earlier line whose step file does not exist yet (the seed read, before any worker wrote) is a normal red ("line N's answer is not there yet"), never an instrument stop; only an unreadable file is a stop.
4372
+ - **The plan card.** One code-worded row per fact under its line, read by the one reader (`checksOfClose` / `factRows`) the confirm card, the Job tab (`planFromSpec`) and the sign-time `planMatchesClose` share, e.g. `fact · price — money in US dollars, at most 450 US dollars, after "Price", quoted from "Flights"`, then the count row. A `~` rule the drafter listed as not checked reads `line N: "<rule as written>" — <reason>`.
4373
+ - **The worker brief.** `rubricGoal({ ..., closeDecl })` adds a code-written brief per line (`lineBriefs`): a facts line gets its facts and its count; a text line that follows gets "every number must be copied exactly as written in a quote you wrote for job line N". `stepFileContract()` names both shapes.
4374
+ - **Web addresses as inputs (Part 1c, `src/pagefetch.js`, `src/source.js`).** An Inputs line may be `label: https://...` (also a rubric card's FIRST line; `looksLikeRubricInput`). `fetchPageText(url)` calls barebrowse `connect({ incognito: true, blockPrivateNetwork: true })` (`CONNECT_OPTIONS`) → `goto` → `waitForNetworkIdle({ idle: 1500, timeout: 20000 })` (bounded; a page that never settles is read as it stands) → `snapshot({ mode: 'act' })`, always closing the browser, with our own 90 s deadline (L1: a load-time `browse()` snapshot gave 77,951, 8,801 or 719 characters of the same results page; L2: a page whose text outside the banner / navigation / contentinfo landmarks is under `MIN_CONTENT_CHARS` = 100 (`pageContentChars`) is a stop too, "only the site's own header and menus", and an input with no content never reaches the look or an early ask: the survey is ABSENT, "holds nothing to read", nothing spent); an empty or under-200-character page (`MIN_PAGE_CHARS`), a bot challenge, an unreachable address, a credentialed or secret-shaped address, or a page carrying a secret shape is a plain-words $0 stop on its line. `proveRubricInputs({ ..., fetchUrl })` freezes the page text under `input/<label>.txt` and records `url` on the manifest input row. A FRESH `run-u --spec` start (not `--resume` / `--door` / `--decide`) calls `refreezeUrlInputs`: it fetches every recorded address again, writes that text into the prepared tree, commits it as the new seed and rewrites `source.json` (new `sha256` and `seed`, `draftSha256` keeps what the look saw) BEFORE the worker starts, so quotes are checked against the run's copy and the worker reads it, never the live page. A failed or empty fetch stops the run, "The run did not start. Nothing spent." Plan card (L3): `settlePersonsWords` (called in `authorClose` before validation) unmarks AI's proposal on a limit made of the person's own numbers, swaps in the one explicit positive time range the person wrote, and drops a `refused` entry for a `~` rule a part of its line carries; the look's fact may carry `forRule` (a rule it numbered from its own knowledge, marked AI's proposal), which removes that rule from the look's not-found list so it is not asked. Test seams: `fetchPageText(url, { connect })`, `deps.fetchUrl` (`run-u`), `deps.fetchUrl` / `deps.lookFn` (`createSession`).
4375
+ - **Ask at once, only about intent (I1).** A look not-found entry that names a `~` rule becomes a question worded by code (`ruleQuestions`: line and rule named), asked before the confirm turn through the existing question gate (`answerQuestions`; `normalizeQuestions` caps it at `MAX_QUESTIONS`); the answer is a new `~` rule on that line. A rubric job on files never offers the piecemeal try-3 question; its failed-draft stop (`draftStopText({ wholeJob: true })`) says to review the whole job and submit it again.
4376
+ - **Known limits (documented, not built around).** Airport codes like SFO have no stable label on a flight page: direction is checkable only through the full airport names the page uses. An unanchored `text` fact cannot tell which field a value sits in. `am` / `pm` in lower case and `9:27AM` are malformed times; `v0.41.0` is one token. A fact step's quote proves the value is in a real record, not that it is the RIGHT record: "the cheapest" and "the last 3" are Not checked. Verified on real files only; the live proof (DeepSeek, both jobs through the panel) has not been run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.40.0",
3
+ "version": "0.42.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",
@@ -17,7 +17,7 @@
17
17
  "license": "Apache-2.0",
18
18
  "author": "hamr0",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=22"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "node scripts/hermetic.mjs && node scripts/prompt-commit-check.mjs --range origin/main..HEAD",
@@ -27,8 +27,9 @@
27
27
  "check:lockfile": "node scripts/check-lockfile.mjs"
28
28
  },
29
29
  "dependencies": {
30
- "bare-agent": "^0.51.0",
31
- "bareguard": "^0.21.0",
30
+ "bare-agent": "^0.52.0",
31
+ "barebrowse": "^0.21.0",
32
+ "bareguard": "^0.24.0",
32
33
  "litectx": "^0.32.0"
33
34
  },
34
35
  "devDependencies": {