bareloop 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bareloop.context.md +63 -11
  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 +5 -2
  9. package/src/authorscout.js +9 -6
  10. package/src/bridges.js +1 -1
  11. package/src/calibrate.js +1 -1
  12. package/src/cardauthor.js +4 -1
  13. package/src/declaredclose.js +4 -3
  14. package/src/draftspend.js +6 -1
  15. package/src/interviewrun.js +56 -27
  16. package/src/job.js +38 -5
  17. package/src/jobblock.js +140 -0
  18. package/src/judged.js +1 -1
  19. package/src/ledger.js +1 -1
  20. package/src/panel/authorroutes.js +15 -5
  21. package/src/panel/authorsession.js +158 -47
  22. package/src/panel/index.html +179 -70
  23. package/src/panel/jobplan.js +107 -0
  24. package/src/panel/server.js +67 -13
  25. package/src/planrun.js +4 -4
  26. package/src/promptregisters.js +1 -1
  27. package/src/providers.js +101 -19
  28. package/src/reuse.js +1 -1
  29. package/src/root.js +1 -1
  30. package/src/run.js +3 -3
  31. package/src/selection.js +1 -1
  32. package/src/source.js +64 -1
  33. package/src/text.js +13 -0
  34. package/src/transport.js +1 -1
  35. package/src/userrun.js +3 -3
  36. package/types/authorflow.d.ts +258 -39
  37. package/types/authoring.d.ts +7 -1
  38. package/types/authorjob.d.ts +35 -3
  39. package/types/authorreadout.d.ts +17 -1
  40. package/types/authorscout.d.ts +5 -3
  41. package/types/declaredclose.d.ts +4 -1
  42. package/types/draftspend.d.ts +8 -2
  43. package/types/job.d.ts +1 -1
  44. package/types/jobblock.d.ts +102 -0
  45. package/types/panel/authorsession.d.ts +50 -14
  46. package/types/panel/jobplan.d.ts +58 -0
  47. package/types/panel/server.d.ts +20 -0
  48. package/types/planrun.d.ts +1 -1
  49. package/types/providers.d.ts +47 -3
  50. package/types/run.d.ts +1 -1
  51. package/types/source.d.ts +26 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to bareloop are documented here. Format:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
+ ## [0.40.0] - 2026-10-11
9
+
10
+ ### Added
11
+
12
+ - **The job block (P7).** A job is now written as two numbered boxes, **The job** and **Inputs**, replacing Goal / Source / Success / Guardrails / Judge examples. A `~` line is a rule for the line above it; `~ PASS:` / `~ FAIL:` lines are a rubric's judge examples and keep their line number. Inputs are numbered `label: value`: line 1 is the repo, later lines are files inside it, proven at $0 before any session. One parser (`src/jobblock.js`) serves the panel and the CLI interview, which now asks The job and Inputs as multi-line answers.
13
+ - **Every job line is covered.** The signed spec carries `jobLines` and `inputs`. Each stage the drafter writes names the job line(s) it serves (`fromLine`, an array, so one check may serve several lines), or the line is listed in `closeDecl.refused` with a reason; an uncovered line is a $0 red fed back through the revise loop. Signed `jobLines` carry `rules[]` beside `rule`, so Reuse never cuts an example at its own `; `. Only new jobs' hashes move; Inputs is open on a Reuse.
14
+ - **Try-3 questions.** On the last drafting try, after two failed ones (validation red, seed-read failure or vacuous close, or an output cut-off), the drafter may ask up to 2 questions, each naming a job line. The panel asks each through the ordinary ask path (answer required, no Skip), adds the answer word for word as a `~` rule on that line, and drafts once more with no asking. Off unless the caller wires an asker (the CLI never does).
15
+ - **Thinking switch for drafting and the judge.** `thinkingMode` ('on' | 'off', anything else throws) is asked of bare-agent; `off` is sent only for models bare-agent's `thinkingSupport` says are switchable, and nothing is sent for a not-switchable or unverified model. The draft log records `thinking` (off / on / not-switchable / unverified) and bare-agent's `thinkingApplied` verdict per call.
16
+ - **Panel.**
17
+ - One shared plan renderer draws the chat bubble and the Job tab: numbered lines, rules, checks under the line they serve, with *Not checked* and *Always on* folds; an "AI's summary" row, and an "AI's open questions" row (each question its own item, then "Revise if you need to.") shown only when there are questions.
18
+ - The Job tab runs The job, Inputs, Destination, Tools, Cap, Signed, with the job at full height; older jobs show an older-job note and keep their goal as line 1.
19
+ - The job boxes grow to 8 lines then scroll, with the number gutter scrolling alongside; the left pane widens to 630px from 1200px wide.
20
+ - The drafting list opens with a code-owned *thinking* row; if the model reports it thought anyway after `off` was sent, it reads "thinking on (the model thought anyway)".
21
+ - Every drafting stop now says why in plain words (what happened, what it cost, what to do).
22
+
23
+ ### Changed
24
+
25
+ - **Thinking is OFF by default for drafting and the judge, for every switchable model, worker untouched.** The default was decided by a frozen, pre-registered 8-draft ON/OFF contrast measured on **deepseek-flash only** (rule said OFF; underpowered by declaration); extending it to every other switchable model is hamr's ruling of 2026-10-11, not a measurement. `on` sends nothing and is byte-identical to a call with no knob. The deepseek-flash request body is unchanged (thinking disabled, now produced by bare-agent); the per-model allow-lists in `src/providers.js` are deleted.
26
+ - A secret-shaped input file is masked, not refused (matching the worktree repo door); `.env` by name and symlinks are still refused.
27
+ - Dependencies: `bare-agent` ^0.49.0 to ^0.51.0.
28
+
29
+ ### Fixed
30
+
31
+ - **Output cut-off is a failed try, not provider-red.** A drafter answer stopped at the output cap (`truncated:max_tokens`) now goes through the same retry ladder as a malformed artifact, with the reason "the AI's answer was cut off at the output limit"; the cut-off reply is not replayed, `AUTHOR_MAX_TOKENS` is not raised, real transport failures stay provider-red, and spend stays booked. An all-cut-off try also counts for the try-3 question gate. Test-proven only, not exercised live.
32
+ - A dropped connection (ECONNRESET "socket hang up") is a call casualty like ETIMEDOUT, and the scout stop says in words that the first look did not complete (F217).
33
+ - `proveInputFiles` compares like-for-like, so a repo behind a symlinked parent proves its Inputs.
34
+
8
35
  ## [0.39.0] - 2026-10-10
9
36
 
10
37
  ### Added
@@ -67,7 +67,7 @@ inherited rule carries the green that minted it and the contrast that attributed
67
67
  an unknown provider name THROWS there, it never falls back to a default. Per-model
68
68
  request-key gating lives in that table: `deepseek-flash` sets bare-agent's
69
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)
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
71
  takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
72
72
  (`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
73
73
  **ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
@@ -179,9 +179,10 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
179
179
  | field | shape | notes |
180
180
  |---|---|---|
181
181
  | `goal` | non-empty text | what the agent plans against |
182
+ | `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
183
  | `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
184
  | `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 |
185
+ | `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
186
  | `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
187
  | `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
188
 
@@ -617,7 +618,7 @@ a step, or test it without a provider.
617
618
 
618
619
  | step | call | what it is |
619
620
  |---|---|---|
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 |
621
+ | 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
622
  | 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
623
  | 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
624
  | 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 +626,49 @@ a step, or test it without a provider.
625
626
  | 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
627
  | the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
627
628
 
629
+ **Drafting stops and try-3 questions (P7, F215).** (1) A drafter round cut off at the output cap (bare-agent tags it
630
+ `error: 'truncated:max_tokens'`, neutral `stopReason: 'max_tokens'`) is a FAILED TRY on `askStructured`'s retry ladder with the
631
+ reason "the AI's answer was cut off at the output limit" (`axis: 'output-truncated'`) — NOT `provider-red`; all attempts stay
632
+ booked, `AUTHOR_MAX_TOKENS` is never raised, and only a real transport failure stays `provider-red`. `draftStopText`
633
+ (`src/authorreadout.js`) words any drafting stop that has no wording of its own; the panel puts it on the failed step. (2)
634
+ `authorClose({ offerQuestions })` (default `false`; the CLI never sets it) offers the optional `questions` property on the LAST
635
+ 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
636
+ from fwdloop's validation-only rule): a seed-read failure is a work stage that instrument-stopped or a close with no work stage
637
+ red at the seed (vacuous), and its lines are the `fromLine`s of the stages named, else every job line (`seedReadFailure`); a try
638
+ 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
639
+ 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
640
+ on to try 3 after an all-cut-off try 2 only when that gate can open, i.e. try 1 failed too);
641
+ the machine keeps at most
642
+ `MAX_QUESTIONS` (2) — each on a job line the previous try's reds named — and logs the rest in `iterations[].droppedQuestions`;
643
+ the drafter is never told a number. It then stops `questions-open` (`questions: [{line, question}]`, nothing validated).
644
+ `authorCloseForJob({ answerQuestions })` is the seam that asks the person (required answer, no skip), takes back the job lines
645
+ with each answer added word for word as a `~` rule, and runs the draft once more over the same survey, listing and confirmed
646
+ plan with the first leg's spend carried as prior spend and `offerQuestions: false`. The confirm turn's own `questions` (the
647
+ plan's open questions) are a separate, unchanged mechanism: they are asked before drafting, about the plan; try-3 questions
648
+ are asked after two failed drafts, about a job line.
649
+
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.
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,
@@ -1307,7 +1351,7 @@ arm replaces the CLI-display cap rather than stacking on it; a non-capping arm l
1307
1351
  installed *inside* the shim, so native never loses its bound. Default-flip pending a paid contrast;
1308
1352
  see CHANGELOG for the replay numbers behind it.
1309
1353
 
1310
- **`scout` (default `true`) — the operator-only off switch** (`docs/product/SCOUT-CONTRAST.md`),
1354
+ **`scout` (default `true`) — the operator-only off switch** (`docs/logs/SCOUT-CONTRAST.md`),
1311
1355
  threaded verbatim from `runJob` to `runPlan`. `false` on a FRESH run (no `resumeSeed`) skips the
1312
1356
  read-only survey entirely — `scout-start`/`scout-result`/`scout-empty` never fire, and a single
1313
1357
  `scout-skipped { reason: 'operator-off', meaning }` records it (a RECORD, never silence — the
@@ -1874,7 +1918,7 @@ hand them to the one `Loop` that drives worker, scout and planner rounds; the ru
1874
1918
  them once. No price on the row = no `rates` key anywhere = the guesstimate above, byte-identical to
1875
1919
  before. bareloop keeps no price list of its own, ever.
1876
1920
 
1877
- **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.49.0`)
1921
+ **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.51.0`)
1878
1922
  carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
1879
1923
  `src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
1880
1924
  `pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
@@ -3302,7 +3346,7 @@ key you want first (the panel picks the row by the Model menu).
3302
3346
  close scripts need at run time. The correct flow is **`cd <bundle> && npm install`** (reads
3303
3347
  the bundle's own `package.json`), then **`./node_modules/.bin/bareloop run . --repo <path>
3304
3348
  …`**. `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
3349
+ before any close ever runs; see `docs/wiki/FINDINGS.md` F128 for the full account
3306
3350
  (grounded in the real crashed spine) and `docs/product/EXPORT-BUILD.md`'s dated addendum.
3307
3351
 
3308
3352
  **Minting-run line, corrected (F128).** The first-run notice used to print the shipped
@@ -3585,8 +3629,7 @@ key you want first (the panel picks the row by the Model menu).
3585
3629
  fills the card through this same path (a pick, then `change` returns to the empty search; Deterministic or Rubric leaves reuse).
3586
3630
  It opens the Chat tab's New job card
3587
3631
  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
3632
+ 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
3633
  (`400 "<Field> is locked on a reused workflow — Clear the card to change it"`; the page is never trusted).
3591
3634
  Changing a locked box is Clear (the Chat card's top-right button), which drafts. Routes, behind `checkHumanGuard`:
3592
3635
  `GET /api/author/start-from?runid=<id>` or `?import=<id>` → `{ok, origin, card, from:'signed job'|'imported job',
@@ -3677,7 +3720,7 @@ key you want first (the panel picks the row by the Model menu).
3677
3720
  live card reads "$X so far · running Ym" exactly like the right pane.
3678
3721
 
3679
3722
  - **`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
3723
+ (the JOBS-table/`--spec` runner, resume, the review door — `docs/wiki/FINDINGS.md`'s
3681
3724
  U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
3682
3725
  **`src/userrun.js`'s own `main(argv, deps)`**, unparsed — that function is the flag
3683
3726
  grammar's ONE owner, and `scripts/run-u.mjs` calls the exact same function directly, so
@@ -3699,7 +3742,7 @@ key you want first (the panel picks the row by the Model menu).
3699
3742
  fed by each flow's `deps.invokedAs`.
3700
3743
 
3701
3744
  - **`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
3745
+ interview, at the terminal, one question at a time (D10; `docs/wiki/FINDINGS.md`'s
3703
3746
  interview surface). `src/cli.js`'s `interview` dispatch hands `rest` straight to
3704
3747
  **`src/interviewrun.js`'s own `main(argv, deps)`**, unparsed — the same one-owner shape
3705
3748
  `run-u` uses, and `scripts/run-interview.mjs` calls the exact same function directly.
@@ -4288,7 +4331,7 @@ blast radius.
4288
4331
 
4289
4332
  ## Gotchas
4290
4333
 
4291
- *Recorded here as they're found (repo-side friction goes to `docs/logs/FINDINGS.md`).*
4334
+ *Recorded here as they're found (repo-side friction goes to `docs/wiki/FINDINGS.md`).*
4292
4335
 
4293
4336
  - **`non-green-verdict` is a misleading refusal verb, and it is kept on purpose.** When
4294
4337
  `runInterview` refuses a job for having no git repository (`repoPath` absent), the refusal
@@ -4309,3 +4352,12 @@ blast radius.
4309
4352
  an append-only record that captures a key captures it forever.
4310
4353
  - **The spine is append-only** and the single source of truth; every UI (panel included)
4311
4354
  is a pure observer of it.
4355
+
4356
+ ## The job block (P7) — "The job" + "Inputs"
4357
+
4358
+ Goal, Success, Guardrails and Judge examples are one box, **The job**, and Source is **Inputs** (CLI interview and panel card alike).
4359
+
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.
4361
+ - **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
+ - **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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.39.0",
3
+ "version": "0.40.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.51.0",
31
31
  "bareguard": "^0.21.0",
32
32
  "litectx": "^0.32.0"
33
33
  },