bareloop 0.39.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 (76) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/bareloop.context.md +137 -112
  3. package/package.json +2 -2
  4. package/src/authorflow.js +333 -58
  5. package/src/authoring.js +65 -2
  6. package/src/authorjob.js +57 -9
  7. package/src/authorreadout.js +50 -2
  8. package/src/authorrun.js +30 -27
  9. package/src/authorscout.js +9 -6
  10. package/src/bridges.js +1 -1
  11. package/src/bundle.js +7 -4
  12. package/src/bundlerun.js +4 -1
  13. package/src/calibrate.js +1 -1
  14. package/src/cardauthor.js +4 -1
  15. package/src/cli.js +1 -1
  16. package/src/config.js +129 -29
  17. package/src/declaredclose.js +4 -3
  18. package/src/draftspend.js +6 -1
  19. package/src/index.js +2 -4
  20. package/src/interviewrun.js +73 -52
  21. package/src/job.js +50 -11
  22. package/src/jobblock.js +140 -0
  23. package/src/judged.js +12 -44
  24. package/src/keysfile.js +15 -7
  25. package/src/kinds.js +2 -2
  26. package/src/ledger.js +1 -1
  27. package/src/modellist.js +84 -0
  28. package/src/monthly.js +23 -43
  29. package/src/panel/authorroutes.js +34 -19
  30. package/src/panel/authorsession.js +187 -72
  31. package/src/panel/index.html +699 -215
  32. package/src/panel/jobplan.js +107 -0
  33. package/src/panel/runroutes.js +3 -0
  34. package/src/panel/server.js +102 -28
  35. package/src/panel/settingsroutes.js +232 -173
  36. package/src/planrun.js +6 -6
  37. package/src/promptregisters.js +1 -1
  38. package/src/providerrows.js +226 -141
  39. package/src/providers.js +58 -250
  40. package/src/providershapes.js +18 -0
  41. package/src/replay.js +1 -1
  42. package/src/reuse.js +1 -1
  43. package/src/root.js +1 -1
  44. package/src/run.js +3 -3
  45. package/src/selection.js +1 -1
  46. package/src/source.js +64 -1
  47. package/src/text.js +13 -0
  48. package/src/thinking.js +130 -0
  49. package/src/transport.js +1 -1
  50. package/src/userrun.js +53 -76
  51. package/types/authorflow.d.ts +258 -39
  52. package/types/authoring.d.ts +7 -1
  53. package/types/authorjob.d.ts +35 -3
  54. package/types/authorreadout.d.ts +17 -1
  55. package/types/authorscout.d.ts +5 -3
  56. package/types/config.d.ts +23 -6
  57. package/types/declaredclose.d.ts +4 -1
  58. package/types/draftspend.d.ts +8 -2
  59. package/types/index.d.ts +1 -1
  60. package/types/job.d.ts +6 -6
  61. package/types/jobblock.d.ts +102 -0
  62. package/types/judged.d.ts +5 -36
  63. package/types/keysfile.d.ts +15 -4
  64. package/types/modellist.d.ts +50 -0
  65. package/types/monthly.d.ts +4 -3
  66. package/types/panel/authorsession.d.ts +44 -15
  67. package/types/panel/jobplan.d.ts +58 -0
  68. package/types/panel/server.d.ts +34 -4
  69. package/types/planrun.d.ts +1 -1
  70. package/types/providerrows.d.ts +192 -99
  71. package/types/providers.d.ts +35 -116
  72. package/types/providershapes.d.ts +10 -0
  73. package/types/run.d.ts +1 -1
  74. package/types/source.d.ts +26 -0
  75. package/types/thinking.d.ts +90 -0
  76. package/types/userrun.d.ts +6 -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 a per-model, judge-only `thinking:{type:'disabled'}` (allow-list `OPENAI_JUDGE_CALL_OPTIONS`, read through `judgeCallOptions`, applied by `defaultJudgeLoop`; F192) because deepseek-flash's default thinking ate the 4000-token locate budget; workers and the drafter 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 |
@@ -179,9 +176,10 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
179
176
  | field | shape | notes |
180
177
  |---|---|---|
181
178
  | `goal` | non-empty text | what the agent plans against |
179
+ | `jobLines`, `inputs` | optional (P7). `jobLines: [{ n, text, rule, rules? }]` — the person's own numbered job lines, `rule` = their `~` lines joined with `'; '` (`''` when none), `rules` = the same `~` lines as an array BESIDE `rule` (present only when the line has any; must join back to `rule` exactly; the Reuse card prefers it so an example's own `; ` is not cut; an older spec lacking it renders by splitting `rule`); `inputs: [{ n, label, value }]` — line 1 the repo (absolute path), lines 2+ tracked files inside it. Exact fields, numbered 1..n in order | what the Job tab and Reuse show back exactly as typed (the signed `goal` stays the model's confirm sentence, which the worker reads). **A spec without them keeps its `jobSpecHash`** (never rewritten); only NEW jobs carry them, so a new job's `workflowKey` differs from an old one's. `inputs` is OPEN on a reuse (`REUSE_OPEN_SPEC_FIELDS`), so `workflowKey` ignores it; `jobSpecHash` covers it. Parsed by `parseJobBlock` / `parseInputs` (`src/jobblock.js`, one owner for the panel and the CLI) |
182
180
  | `verdictType` | `green` \| `soft-green` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **AUTHORING offers both (`MENU_CLASSES`).** `soft-green` has its judged floor (`judged-floor`, signed rubric card + calibration gate) and is the forward path for a job needing judgement, paired with the end-of-run review door. Every `request-red` (e.g. for a locked TOOL verb, not a verdict class) also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
183
181
  | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs?, direction? }, ...]` (PRD v1.28), or a close object (table below) | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; an object close names no command to run, so the plan flow refuses one at runtime as `close-unsupported`. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
184
- | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params }], notes? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
182
+ | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params, fromLine? }], notes?, refused? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. **Job-line coverage (P7, `checkJobLines` in `validateDeclaration`, $0, fed back through the revise loop):** with `jobLines` handed in, every stage the drafter writes carries `fromLine: [N, ...]` — a non-empty, duplicate-free ARRAY of real job lines, so one check (e.g. the one judged stage) may serve several lines, the mandatory guards carry none, and every numbered line has at least one stage OR a `{line, reason}` in `closeDecl.refused` — a note in `notes` does NOT cover a line (`job-line-uncovered`). `validateCloseDecl`/`validateJob` run it when the spec carries `jobLines`. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
185
183
  | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
186
184
  | `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
187
185
 
@@ -362,7 +360,7 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
362
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 |
363
361
  | `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
364
362
  | `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
365
- | `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 |
366
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) |
367
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 |
368
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 |
@@ -617,7 +615,7 @@ a step, or test it without a provider.
617
615
 
618
616
  | step | call | what it is |
619
617
  |---|---|---|
620
- | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. The interview ADMITS only what is on the menu (`MENU_CLASSES`): nothing is locked today (`LOCKED_CLASSES` is empty), and any pick off the menu returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. The interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost several slots since it was frozen (D13's genre confirm; the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input; and, at PRD item 33 M3 piece 3, the old "which files change/read" question, replaced by Source and Destination — MECHANICAL fields, proven against the machine, never a numbered answer), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is THREE questions (Goal, Success, Guardrails); the `soft-green` set is those three plus one more — Judge Examples (one example to pass, one to fail, and why) — the judged floor's one required input, compiled into the signed rubric card and the frozen calibration set. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
618
+ | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. The interview ADMITS only what is on the menu (`MENU_CLASSES`): nothing is locked today (`LOCKED_CLASSES` is empty), and any pick off the menu returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. The interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost several slots since it was frozen (D13's genre confirm; the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input; and, at PRD item 33 M3 piece 3, the old "which files change/read" question, replaced by Source and Destination — MECHANICAL fields, proven against the machine, never a numbered answer), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today (P7) the green set is ONE question — "The job", the person's numbered lines with their `~` rules, rendered by `renderJobLines` (`src/jobblock.js`); the `soft-green` set is that one plus Judge Examples, DERIVED from the `~ PASS:` / `~ FAIL:` lines of the same block (`renderExamples`, each example kept with its line number) — the judged floor's one required input, compiled into the signed rubric card and the frozen calibration set. Inputs (`label: value` lines) travel beside the answers as `inputs`, and `authorCloseForJob({ jobLines, inputs })` tells the drafter "the person pointed at these inputs: N label path" and orders `fromLine` tagging. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
621
619
  | survey | `runAuthorScout({ workdir, provider, attempts?, ceilingUsd? })` → `{ state, facts, reason, meta, raw, raws, calls, budgetStop }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind. **`ceilingUsd` is the operator's money ceiling** (PRD v1.62) — checked BETWEEN attempts and before F59's reserved round, so no paid call escapes it; `null`/absent is UNBOUNDED, and a non-null non-finite value THROWS (the shared `capStop` seam — see `authorClose` below). When it ends the ladder, `budgetStop` names which stop (`cap-halt` \| `pricing-red`) and a survey that was never asked for carries the typed cause `not-funded` — deliberately OUTSIDE `SCOUT_RETRY_CAUSES`, because a retry is precisely what the cap forbade |
622
620
  | listing | `buildSeedListing({ workdir, seedRef, sourcePaths, testPaths })` | mechanical, `$0`, no model. `files` is the WHOLE tree (what the validator judges paths against); `block` is scoped to the survey's own paths and capped in ANNOUNCED tiers (what the prompt carries). Handing the validator the scoped half would make a job scoped to `src/` read as whole-tree and silently disarm the one-population law |
623
621
  | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does. **F176 (fixed):** "kept the last accepted revision" is not the same claim as "kept the best revision" — a later revise round can VALIDATE fine yet MEASURE worse (every stage instrument-stops rather than reading a real red/green verdict). `declaration`/`seedRead`/`finalFrom`/the returned `genreEnv.dropped` are therefore the NEWEST SOUND measured iteration, not simply the last one, when the last one instrument-stopped and an earlier one did not; `stop` itself never changes. The swap is never silent: the return also carries **`fellBack`** — `null` on every run where it never fired, else `{from, to, brokenStages}` naming which iteration was dropped, which now stands, and which of the dropped one's stages instrument-stopped — reported through `onPhase('author-fallback', …)` as it happens and printed at the terminal readout (`fellBackLines`, `src/authorreadout.js` — lifted out of `scripts/author-readout.mjs` by PANEL-BUILD.md P0; called from `src/authorrun.js`). **F179/F180 (history):** bare-agent 0.42.0's OpenAIProvider used to THROW mid-call on malformed tool-call JSON (a raw SyntaxError, after the billed round), crashing past this function; a 2026-09-15 stopgap caught it as a `provider-red`/`ask.providerError` casualty (booked `costUsd: null`) so a sound prior declaration was kept via the fallback above rather than discarded. **F179 (bare-agent 0.43.0, BA-27 — the real mechanism):** the throw is fixed upstream. `OpenAIProvider`/`OllamaProvider.generate` now return a malformed round PRICED (real `usage`, never `costUsd: null`) with `toolCalls: []` plus their own `malformedToolCall: {name, error}` marker, which `Loop.run` surfaces unchanged on its return. For the authoring declaration and confirm-turn calls, `askStructured` reads that marker and RETRIES through the existing malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`) instead of stopping the run — nothing is ever repaired, and a malformed FIRST call no longer needs an earlier sound declaration to fall back to. The generic provider-casualty fallback above (idle-timeout, etc.) still applies for every other admitted class. **F184 (fixed, worker path):** the scout and the worker path never joined that retry ladder (still true — retrying is arbiter-adjacent, out of scope), but the worker path's `ask()` (`src/planrun.js`) now at least READS the marker where it already sees it and emits a distinct spine record, `worker-malformed-tool-call` (`{phase, iteration, name, error}`), before returning `r` unexamined exactly as before — no retry, no change to strikes/ladder/attempt counting or verdict routing, purely the same visibility bar this section's authoring-path fix already set |
@@ -625,6 +623,52 @@ a step, or test it without a provider.
625
623
  | assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `src/authorrun.js` (lifted out of `scripts/run-author.mjs` by PANEL-BUILD.md P0) asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
626
624
  | the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
627
625
 
626
+ **Drafting stops and try-3 questions (P7, F215).** (1) A drafter round cut off at the output cap (bare-agent tags it
627
+ `error: 'truncated:max_tokens'`, neutral `stopReason: 'max_tokens'`) is a FAILED TRY on `askStructured`'s retry ladder with the
628
+ reason "the AI's answer was cut off at the output limit" (`axis: 'output-truncated'`) — NOT `provider-red`; all attempts stay
629
+ booked, `AUTHOR_MAX_TOKENS` is never raised, and only a real transport failure stays `provider-red`. `draftStopText`
630
+ (`src/authorreadout.js`) words any drafting stop that has no wording of its own; the panel puts it on the failed step. (2)
631
+ `authorClose({ offerQuestions })` (default `false`; the CLI never sets it) offers the optional `questions` property on the LAST
632
+ try of the full ladder only, and only when the two tries before it both failed — validation OR the seed read (hamr ruled 2026-10-10; this DIFFERS
633
+ from fwdloop's validation-only rule): a seed-read failure is a work stage that instrument-stopped or a close with no work stage
634
+ red at the seed (vacuous), and its lines are the `fromLine`s of the stages named, else every job line (`seedReadFailure`); a try
635
+ whose EVERY structure attempt was cut off at the output cap also counts as failed (hamr ruled 2026-10-10; a cut-off names no line, so
636
+ every job line counts), while a try cut off once but finished by a later attempt is judged by that finished attempt (the ladder carries
637
+ on to try 3 after an all-cut-off try 2 only when that gate can open, i.e. try 1 failed too);
638
+ the machine keeps at most
639
+ `MAX_QUESTIONS` (2) — each on a job line the previous try's reds named — and logs the rest in `iterations[].droppedQuestions`;
640
+ the drafter is never told a number. It then stops `questions-open` (`questions: [{line, question}]`, nothing validated).
641
+ `authorCloseForJob({ answerQuestions })` is the seam that asks the person (required answer, no skip), takes back the job lines
642
+ with each answer added word for word as a `~` rule, and runs the draft once more over the same survey, listing and confirmed
643
+ plan with the first leg's spend carried as prior spend and `offerQuestions: false`. The confirm turn's own `questions` (the
644
+ plan's open questions) are a separate, unchanged mechanism: they are asked before drafting, about the plan; try-3 questions
645
+ are asked after two failed drafts, about a job line.
646
+
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
+
671
+
628
672
  **D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
629
673
  CLASS** (PRD v1.57 §2 — what a battery is FOR is the class of dishonesty that class of
630
674
  verdict admits). `CLASS_BATTERIES` is the attachment point (structure, un-removability,
@@ -906,13 +950,12 @@ down.
906
950
  `--base-url` is OPTIONAL with NO default (PRD item 33 close-out — L17 named the provider but
907
951
  never admitted the endpoint): a provider name's table entry (`src/providers.js`) can be reached
908
952
  through an OpenAI-compatible gateway other than its own default host — DeepSeek, today's one
909
- 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,
910
954
  the draft carries NO `baseUrl` field at all (never `null`, never `''`) and every provider
911
955
  constructor defaults its own endpoint. When given, it lands in the draft's `baseUrl` field
912
956
  beside `provider` and passes through the SAME `validateJob` pass every other field takes below,
913
957
  at $0: `http://` to a public host, an embedded `user:pass@`, or an unparseable string reds there,
914
- before anything is written. The key still follows the PROVIDER, not the endpoint — a DeepSeek
915
- 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.
916
959
 
917
960
  **Source and Destination are the interview's own FIRST TWO QUESTIONS, and they REPLACE
918
961
  `--patient`** (PRD item 33 M3, ruling 2, `docs/product/ITEM33-BUILD.md`). Source — "a local
@@ -977,8 +1020,8 @@ spends it.
977
1020
 
978
1021
  **It ends by OFFERING the paid step, and the offer's default is NO.** `run-author.mjs` is a
979
1022
  DIFFERENT PROCESS under its own ceiling; the interview prints its exact command line (with
980
- `--budget` propagated only when one was given, and a note when the CHOSEN provider's own key —
981
- `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
982
1025
  `run-author.mjs` and `run-u.mjs` share — is unset in this shell, OR set but MALFORMED: F181 — a
983
1026
  value carrying a line break/control character/stray whitespace (a two-line secret-store entry,
984
1027
  e.g.) reads a distinct reason via `apiKeyProblem` (`src/providers.js`), never the value)
@@ -1307,7 +1350,7 @@ arm replaces the CLI-display cap rather than stacking on it; a non-capping arm l
1307
1350
  installed *inside* the shim, so native never loses its bound. Default-flip pending a paid contrast;
1308
1351
  see CHANGELOG for the replay numbers behind it.
1309
1352
 
1310
- **`scout` (default `true`) — the operator-only off switch** (`docs/product/SCOUT-CONTRAST.md`),
1353
+ **`scout` (default `true`) — the operator-only off switch** (`docs/logs/SCOUT-CONTRAST.md`),
1311
1354
  threaded verbatim from `runJob` to `runPlan`. `false` on a FRESH run (no `resumeSeed`) skips the
1312
1355
  read-only survey entirely — `scout-start`/`scout-result`/`scout-empty` never fire, and a single
1313
1356
  `scout-skipped { reason: 'operator-off', meaning }` records it (a RECORD, never silence — the
@@ -1790,9 +1833,7 @@ omitted is the cold path and byte-identical to a run nobody resumed.
1790
1833
  F87; the field stays legal and one-token reversible, and the narrowing binds only what the
1791
1834
  AGENT may express, never what the operator may run).
1792
1835
  The tier menu is signed into the schema; mapping a tier
1793
- onto a real model (and any per-model provider params) is the RUNNER's territory — nothing
1794
- forces the `sonnet` tier onto any particular model, and running haiku as your default worker
1795
- 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
1796
1837
  `runJob` and `runPlan` take `providerFor?: (tier: string) => provider`. It is called ONLY for
1797
1838
  a step that declared a tier, once per such step — the scout, the plan drafter and every
1798
1839
  untiered step stay on `opts.provider`, so a caller that never plans tiers can omit it
@@ -1867,14 +1908,14 @@ bare-agent's `Loop` takes `rates: {in, out, cacheReadMult?, cacheWriteMult?}`
1867
1908
  tokens, the two multipliers applying to the input rate for the cache-read and cache-write
1868
1909
  tiers (defaulting to Anthropic's 0.1× / 1.25×). A caller-supplied rate is recorded as
1869
1910
  VOUCHED rather than as a guess, and it silences bare-agent's own guesstimate warning.
1870
- **Where bareloop takes it from:** the CUSTOMER's own price on the key's row in
1871
- `~/.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
1872
1913
  tokens — see "Settings" below). `runJob` / `runPlan` accept `rates` (and `judgeRates`) as data and
1873
1914
  hand them to the one `Loop` that drives worker, scout and planner rounds; the run doors resolve
1874
1915
  them once. No price on the row = no `rates` key anywhere = the guesstimate above, byte-identical to
1875
1916
  before. bareloop keeps no price list of its own, ever.
1876
1917
 
1877
- **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.49.0`)
1918
+ **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.52.0`)
1878
1919
  carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
1879
1920
  `src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
1880
1921
  `pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
@@ -3210,7 +3251,7 @@ worktree, before the engine is ever reached.
3210
3251
  some other module's own generic `main`). `cliMain(argv, deps)` is the in-process test/
3211
3252
  integration seam: pass `deps.provider` (and optionally `providerFor`/`judgeProvider`) and the
3212
3253
  real key check is skipped entirely — a scripted provider *is* the run, the
3213
- 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`,
3214
3255
  `now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
3215
3256
 
3216
3257
  Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
@@ -3302,7 +3343,7 @@ key you want first (the panel picks the row by the Model menu).
3302
3343
  close scripts need at run time. The correct flow is **`cd <bundle> && npm install`** (reads
3303
3344
  the bundle's own `package.json`), then **`./node_modules/.bin/bareloop run . --repo <path>
3304
3345
  …`**. `checkBundleDeps` (above) is the fail-safe that catches the wrong-shape install
3305
- before any close ever runs; see `docs/logs/FINDINGS.md` F128 for the full account
3346
+ before any close ever runs; see `docs/wiki/FINDINGS.md` F128 for the full account
3306
3347
  (grounded in the real crashed spine) and `docs/product/EXPORT-BUILD.md`'s dated addendum.
3307
3348
 
3308
3349
  **Minting-run line, corrected (F128).** The first-run notice used to print the shipped
@@ -3449,7 +3490,7 @@ key you want first (the panel picks the row by the Model menu).
3449
3490
  (`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
3450
3491
  `authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
3451
3492
  `bareloop interview`/`bareloop author` already make — never a script's own readline loop,
3452
- 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
3453
3494
  before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
3454
3495
  completion; the page sees the key's NAME and a status word, never the value). `POST
3455
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'`:
@@ -3530,8 +3571,9 @@ key you want first (the panel picks the row by the Model menu).
3530
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
3531
3572
  show the word only; no surface says "done".
3532
3573
  **Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
3533
- `checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
3534
- (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
3535
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
3536
3578
  used) — refused at $0 before any write or spawn — or the monthly $
3537
3579
  limit (same library check and text as Sign & run). If a cap changed, the new spec is written as
@@ -3585,8 +3627,7 @@ key you want first (the panel picks the row by the Model menu).
3585
3627
  fills the card through this same path (a pick, then `change` returns to the empty search; Deterministic or Rubric leaves reuse).
3586
3628
  It opens the Chat tab's New job card
3587
3629
  filled from the SIGNED job: a NEW run (new runid), never a rerun, nothing drafted ($0). **Only five boxes are
3588
- open — Source, Destination, Model, $ cap, Time cap**; every other box (check type, model, job name, goal, success,
3589
- guardrails, judge examples) is the signed workflow, greyed, and the SERVER refuses a start that changed one
3630
+ open — Inputs, Destination, Model, $ cap, Time cap**; every other box (check type, job name, The job) is the signed workflow, greyed, and the SERVER refuses a start that changed one
3590
3631
  (`400 "<Field> is locked on a reused workflow — Clear the card to change it"`; the page is never trusted).
3591
3632
  Changing a locked box is Clear (the Chat card's top-right button), which drafts. Routes, behind `checkHumanGuard`:
3592
3633
  `GET /api/author/start-from?runid=<id>` or `?import=<id>` → `{ok, origin, card, from:'signed job'|'imported job',
@@ -3677,7 +3718,7 @@ key you want first (the panel picks the row by the Model menu).
3677
3718
  live card reads "$X so far · running Ym" exactly like the right pane.
3678
3719
 
3679
3720
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3680
- (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
3721
+ (the JOBS-table/`--spec` runner, resume, the review door — `docs/wiki/FINDINGS.md`'s
3681
3722
  U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
3682
3723
  **`src/userrun.js`'s own `main(argv, deps)`**, unparsed — that function is the flag
3683
3724
  grammar's ONE owner, and `scripts/run-u.mjs` calls the exact same function directly, so
@@ -3686,11 +3727,11 @@ key you want first (the panel picks the row by the Model menu).
3686
3727
  `resolved-spec.json`, F185) — exactly one, never both, never neither; `--resume
3687
3728
  <runid|path>`; `--door <runid|path>` with `--decide <rerun|accept|pause>` and (`rerun`
3688
3729
  only) `--text <…>`; `--review-door`; `--approve <specHash>`; `--registry <dir>` +
3689
- `--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
3690
3731
  <on|off>`. No key/no injected `deps.provider`: prints the preview (invocation hints, the
3691
- right key name for the job's own provider) and exits `0`, spending nothing — the same
3692
- preview `scripts/run-u.mjs --job <x>` prints with no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
3693
- 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
3694
3735
  over this same `src/userrun.js:main`, not rewired to go through `src/cli.js`.
3695
3736
  **Printed commands are spelled the way you entered:** every re-invocation these flows print (the approve
3696
3737
  line, resume, pause, reopen, the interview's run-author line, the author's run-u line) reads `bareloop
@@ -3699,7 +3740,7 @@ key you want first (the panel picks the row by the Model menu).
3699
3740
  fed by each flow's `deps.invokedAs`.
3700
3741
 
3701
3742
  - **`bareloop interview <flags…>`** (PANEL-BUILD.md P0 task 3/4) → the close-authoring
3702
- interview, at the terminal, one question at a time (D10; `docs/logs/FINDINGS.md`'s
3743
+ interview, at the terminal, one question at a time (D10; `docs/wiki/FINDINGS.md`'s
3703
3744
  interview surface). `src/cli.js`'s `interview` dispatch hands `rest` straight to
3704
3745
  **`src/interviewrun.js`'s own `main(argv, deps)`**, unparsed — the same one-owner shape
3705
3746
  `run-u` uses, and `scripts/run-interview.mjs` calls the exact same function directly.
@@ -3809,37 +3850,26 @@ Two files in `~/.config/bareloop/`, outside any repo:
3809
3850
  - **`config.json` — every setting, never a key value.** `readConfig({ home })` /
3810
3851
  `updateConfig(patch, { home })` (atomic tmp + rename, mode 600, unknown fields kept).
3811
3852
  Fields: `monthlyLimitUsd` (number above 0; absent or `null` = no limit; any other value present
3812
- is a `ConfigError`, refused like an unreadable file), `anthropicBalanceNote` (a note the
3813
- 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
3814
3856
  string anywhere in the document is refused on save. An unreadable file is a `problem` string and
3815
3857
  a refused run start ($0), never "no limit". There is no monthly TIME limit (dropped).
3816
3858
 
3817
- **Your own price — `keys.<ENV NAME>.priceInPerM` / `priceOutPerM`.** The customer sets the price of the
3818
- model behind a key, on that key's row, in USD **per 1M tokens** (input and output), for example
3819
- `"keys": { "DEEPSEEK_API_KEY": { "priceInPerM": 0.006, "priceOutPerM": 1.2 } }`. There is no built-in
3820
- price list. Both numbers or neither: each must be a finite number 0 or more, and one alone, a negative,
3821
- a string or `null` refuses the run at $0 (`ConfigError`, exit 2, "Nothing spent.") — a bad price never
3822
- falls back to the guess. A Settings save keeps the two fields (the Settings SCREEN has no price field
3823
- yet; edit `config.json`). `ratesFor(provider, baseUrl, rows, model)` (`src/providerrows.js`) is the one
3824
- lookup, on the SAME row `keyNameFor` picks the key from (`findRow`: shape + endpoint + model), returning
3825
- `null` (no price) or `{ rates: {in, out}, inPerM, outPerM, envName }` with `rates` in bare-agent's
3826
- per-1K unit (`perM / 1000`). The run doors resolve it once, at $0, before the monthly claim and any
3827
- worktree or spine (`src/userrun.js`; `src/authorrun.js`; the panel's authoring session): the worker row
3828
- prices worker, scout and planner rounds; the judge is priced at ITS model's own row — for a different
3829
- provider its own row, for the worker's provider the row on the worker's endpoint named for the judge's model
3830
- (`judgeRatesFor`, `src/providerrows.js`), falling back to the worker's price when the model names no row.
3831
- (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
3832
- no price it stays the built-in guess, as before. The run preview and the run tail print
3833
- `price yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)` when a price is set, and
3834
- 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
3835
- on the dollars your price produces, so a price set too low weakens the cap (a run can do far more
3836
- than you meant before it halts). When a vendor lists two prices (for example a cache-hit and a
3837
- cache-miss input rate) enter the higher one. bare-agent applies its own cache multipliers (read 0.1×,
3838
- write 1.25× of the input rate) to cache tokens; they are not configurable here, so cache tokens are
3839
- priced from your input price by that rule. A price applies from the next run: **past runs are never
3840
- re-priced** (a spine and the run list keep the dollars they booked). Not covered by a price: a native
3841
- (CLI) session, which reports its own cost, and the reuse engine (`runReuse` / `selectBridge`), which
3842
- 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.
3843
3873
 
3844
3874
  **Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
3845
3875
  atLeast, heldUsd, heldRuns }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals>
@@ -3883,45 +3913,31 @@ real spend.
3883
3913
 
3884
3914
  **Money & limits tab** (`GET/POST /api/settings/money`): two tiles ($ and tokens, this month / to date, a
3885
3915
  `≥` prefix when a figure is a floor) and a per-provider table ($, minutes, tokens; a run is grouped
3886
- 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
3887
3917
  change (no Save button); blank clears it. `POST` needs an explicit `monthlyLimitUsd` key (a number
3888
3918
  above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, never "clear the limit".
3889
3919
 
3890
- **Providers tab (P4b, `src/providerrows.js`).** One row per key in `.env` that HAS A VALUE — nothing is
3891
- hardcoded; an empty line is no row, a removed line drops its row on Reload keys. A missing `.env` is
3892
- created (mode 600) on the first Providers read with five empty preset lines (`ANTHROPIC_API_KEY`,
3893
- `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `LOCAL_API_KEY`); an existing file is never
3894
- edited. A value of `null` counts as SET and means "no real key" (the provider is handed the
3895
- placeholder `no-key-needed`, never the word `null`). Each row: the env NAME (read-only) + found,
3896
- **Name** (the model id Chat uses), **API shape** (Anthropic / OpenAI-compatible / Gemini), **Base
3897
- URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display only), and Balance
3898
- (DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
3899
- `keys.<ENV NAME> = { name, shape, baseUrl, priceInPerM?, priceOutPerM? }`. The last two columns are the customer's own price
3900
- (**In $/1M**, **Out $/1M**, plain boxes; blank = not set, a number >= 0 otherwise; the server refuses a half-set pair with a plain
3901
- error and saves nothing, and the page saves NOTHING in a row with exactly one price box filled, showing "Fill both price boxes
3902
- or clear both." when the person leaves that box, until both boxes are filled or both cleared).
3903
- **Open keys folder** (`POST /api/settings/open-keys-folder`) opens `~/.config/bareloop` in the file manager (`xdg-open` / `open`,
3904
- argv array, never the file itself); where it cannot, the page shows the path as text. Defaults for a key with no saved entry:
3905
- `ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
3906
- OpenAI-compatible / `https://api.deepseek.com/v1`; `OPENAI_API_KEY` OpenAI-compatible;
3907
- `GEMINI_API_KEY` Gemini; `LOCAL_API_KEY` OpenAI-compatible / `http://127.0.0.1:11434/v1` (Ollama, LM
3908
- Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **The
3909
- only price shown is the customer's own, typed in the two price boxes** (cost recording, the estimated provenance, spend sums, caps
3910
- and the monthly limit are unchanged; a local model has no known rate, so its rounds come out as a
3911
- non-zero loud guess, never a real $0).
3912
-
3913
- `providerrows.js` is the ONE owner of "model → key + shape + URL": `keyRows`, `findRow` (rows on the
3914
- same shape + endpoint; the Name breaks a tie), `modelChoiceFor` / `chatModels` (Chat's Model menu IS
3915
- the rows; a blank Name is not offered), `keyNameFor` / `applyConfiguredKey` (a job on `(provider,
3916
- baseUrl[, model])` reads the matching row's variable, else the provider's built-in one). All doors
3917
- (`run-u`, `interview`, `author`, the panel's drafting) use it. Routes (all behind the per-start token
3918
- and Origin check; the chat can never reach them): `GET /api/settings/providers`, `POST .../row`
3919
- (`{envName, name, shape, baseUrl}`), `POST .../balance-note`, `POST .../test` (one
3920
- `checkProviderReachable` models-list GET at the row's shape + URL, $0; a refused connection reads
3921
- `nothing answering at <URL>`), `GET .../balance?env=`
3922
- (DeepSeek-hosted row, server-side), and `GET /api/author/models` (Chat's Model menu). The old
3923
- `providers.<row>.key` config field and the key dropdown are gone. Like the authoring routes, a POST
3924
- 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.
3925
3941
 
3926
3942
  ### The source front door — a plain folder/file/URL, no `--patient` (PRD item 33/M2, `src/source.js`)
3927
3943
 
@@ -4288,7 +4304,7 @@ blast radius.
4288
4304
 
4289
4305
  ## Gotchas
4290
4306
 
4291
- *Recorded here as they're found (repo-side friction goes to `docs/logs/FINDINGS.md`).*
4307
+ *Recorded here as they're found (repo-side friction goes to `docs/wiki/FINDINGS.md`).*
4292
4308
 
4293
4309
  - **`non-green-verdict` is a misleading refusal verb, and it is kept on purpose.** When
4294
4310
  `runInterview` refuses a job for having no git repository (`repoPath` absent), the refusal
@@ -4309,3 +4325,12 @@ blast radius.
4309
4325
  an append-only record that captures a key captures it forever.
4310
4326
  - **The spine is append-only** and the single source of truth; every UI (panel included)
4311
4327
  is a pure observer of it.
4328
+
4329
+ ## The job block (P7) — "The job" + "Inputs"
4330
+
4331
+ Goal, Success, Guardrails and Judge examples are one box, **The job**, and Source is **Inputs** (CLI interview and panel card alike).
4332
+
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.
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.
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.
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.39.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.49.0",
30
+ "bare-agent": "^0.52.0",
31
31
  "bareguard": "^0.21.0",
32
32
  "litectx": "^0.32.0"
33
33
  },