bareloop 0.25.1 → 0.27.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,531 @@ 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.27.0] — 2026-09-21
9
+
10
+ ### Changed
11
+
12
+ - **`bare-agent` bumped `^0.42.0` → `^0.43.0`.** Ships BA-27 (bareloop's own corroborating
13
+ upstream ask, `docs/product/UPSTREAM-ASKS.md`): `OpenAIProvider`/`OllamaProvider.generate` no
14
+ longer throw a raw `SyntaxError` when a model's tool-call `arguments` string is malformed JSON —
15
+ the shared `parseToolCalls` helper returns the round priced (real `usage`) with `toolCalls: []`
16
+ plus its own `malformedToolCall: {name, error}` marker instead, and `Loop.run` surfaces that
17
+ marker unchanged on its return. No other package changed (`package-lock.json` diff is
18
+ `bare-agent` only).
19
+
20
+ ### Removed
21
+
22
+ - **`withMalformedToolCallShim`, bareloop's local F179 stopgap (`src/authorflow.js`, added
23
+ 2026-09-15 commit `96866d5`).** Deleted now that bare-agent 0.43.0 fixes BA-27 upstream —
24
+ `makeLoopGenerate` calls `loop.run()` directly again, no delegate provider. `askStructured`'s
25
+ `r?.malformedToolCall` check (`e9e2839`) is unchanged and now reads bare-agent's own field.
26
+ - **`callCasualty`'s SyntaxError admission (`src/text.js`)**, dead code once no provider bareloop
27
+ constructs (`AnthropicProvider`, `OpenAIProvider`, `GeminiProvider` — `src/providers.js`) can
28
+ still throw a JSON `SyntaxError` out of `generate()` after a billed round: verified against
29
+ 0.43.0's installed source that Anthropic and Gemini read tool-call arguments as already-parsed
30
+ objects (nothing to `JSON.parse` in their response paths), and every provider's raw-HTTP-body
31
+ parse is already try/catch-wrapped inside `_request`. The idle-timeout (ETIMEDOUT/TimeoutError)
32
+ admission is unchanged.
33
+
34
+ ### Fixed
35
+
36
+ - **The publish workflow now fails when `package-lock.json`'s version drifts from `package.json`.** npm writes that field on install, so a release that bumps `package.json` without running one leaves it behind — and nothing caught it: `npm ci` fails when the lockfile's *dependency* entries disagree, but never checks the lockfile's copy of the project's own version. `scripts/check-lockfile.mjs` (`npm run check:lockfile`) compares both places npm writes it and runs in the publish workflow. No lockfile is not a failure.
37
+
38
+ - **F191 — a plain-folder source's confirm turn crashed live (run `mu4hc7sp`), leaving a
39
+ two-record spine (`author-start`, `author-phase confirm`) with no ending at all.**
40
+ `confirmProtections` computes protections via `classGuards` (`src/authoring.js`), which is
41
+ keyed by CODE LANGUAGE and throws on a plain folder's `lang: 'none-detected'` — a plain
42
+ folder has no language, so a confirm turn cannot honestly show real, code-derived protections
43
+ for one. `scripts/run-author.mjs` no longer runs a confirm turn over a plain folder at all:
44
+ the honest "no checks yet" stop now fires immediately after `author-start`, at $0 — no scout,
45
+ no confirm turn, no model call. The now-unreachable confirm-turn branch (~70 lines) is
46
+ deleted. Alongside it, the try/catch crash net (which used to start ~300 lines after
47
+ `author-start`, leaving exactly the gap this crash fell into) now opens immediately after the
48
+ spine exists, and its crash message reads the run's own metered-call list rather than
49
+ unconditionally claiming "died inside the paid span". Customer-facing text in
50
+ `scripts/run-interview.mjs`/`bareloop.context.md` that promised a plain-folder confirm turn
51
+ is corrected to match. Fail-first proven in tests (each of the three code changes shown red
52
+ against the prior commit, green after); a new live end-to-end test proves zero provider calls.
53
+ Three follow-up fixes landed in the same pass: the stop's own text claimed "Nothing was spent
54
+ and nothing was written," while the same branch writes `authored.json` into `--out` — both this
55
+ message and `scripts/run-interview.mjs`'s matching install-gap line now read "Nothing was spent.
56
+ Your source was not changed." (true on both counts: no provider call happens on this path, and
57
+ the source tree itself is never touched); an F71 regression (`process.exit(1)` right after a
58
+ `console.log`, which can discard already-queued stdout under a slow reader) was fixed back to
59
+ `process.exitCode = 1`; and a duplicate `spine <path>` print this exitCode fix's fall-through
60
+ briefly introduced was removed (the shared tail print already covers this exit path). Not yet
61
+ re-run against the original crashing patient (`docs/logs/FINDINGS.md` F191).
62
+ - **F190 — fixed properly this pass.** The prior fix (commit 56bbee8) hand-copied
63
+ `scripts/run-u.mjs`'s same-provider `baseUrl` conditional a second time. Both the author and
64
+ judge provider constructions in `scripts/run-author.mjs` now go through `buildRunnerProviders`
65
+ (`src/providers.js`), the one seam `src/cli.js` already used — this file was the one call
66
+ site that had not adopted it. Not yet re-proven live (`docs/logs/FINDINGS.md` F190).
67
+ - **F185 — an interview-authored repo job had no code path INTO `scripts/run-u.mjs`: a person
68
+ finishing the authoring interview was left holding a signed, hash-stable `resolved-spec.json`
69
+ with no way to run it, only a developer hand-step (`cp` the spec into `jobs/` + hand-add a
70
+ `JOBS` row).** hamr's ruling (2026-09-16, option A): `scripts/run-u.mjs` now accepts `--spec
71
+ <path to resolved-spec.json>` as an alternative to `--job <key>` (giving both, or neither, is a
72
+ loud $0 refusal). It `validateJob`'s the spec explicitly and early, then reads the workdir and
73
+ seed off the prepared copy's OWN source manifest (`readSourceManifest`, `src/source.js` — the
74
+ same reader `run-author.mjs` already uses) — the one `source-*/` sibling directory beside the
75
+ spec that carries a `source.json`; never typed, guessed, or defaulted, and a missing/ambiguous
76
+ manifest or a gone tree each refuse by name. `scripts/run-author.mjs`'s "SIGNING PREPARED — NOT
77
+ SIGNED" end screen now prints the exact ready-to-paste run command (real spec path, real
78
+ `--approve <hash>`, the provider's real env-key name per F187). Every existing arbiter gate —
79
+ the `--approve` signature, the budget/wall ceilings, coldReset, the close-first precheck —
80
+ applies unchanged; `--spec` only NAMES the job. Not yet proven live (`docs/logs/FINDINGS.md`
81
+ F185).
82
+ - **F183 — the confirm turn's model-facing prompt never showed the model the real, code-derived
83
+ guards or write fence, so `notChecked` could (and twice live did, runs `mu2bjmed` and
84
+ `mu2qmept`) falsely claim an already-enforced protection was missing** (e.g. "no ts-ignore
85
+ comments" and "do not edit or delete tests", while `no-suppressions` and the write fence
86
+ already covered both). hamr's ruling A (2026-09-16): feed the real protections in as facts,
87
+ never drop the model's list. `confirmProtections`'s existing code-derived list (the same one
88
+ already shown to the person, `src/authorflow.js`) is now computed once, before the model
89
+ drafts, and threaded into `confirmPrompt`'s model-facing prompt as an "ALREADY COVERED" block;
90
+ `CONFIRM_SYSTEM` now orders the model to check that list before naming anything in
91
+ `notChecked`. The model still authors `notChecked` freely — nothing made deterministic, no
92
+ second hand-typed guard list. Not yet proven live (`docs/logs/FINDINGS.md` F183).
93
+ - **Item 6 — `src/authoring.js`'s `TYPES_GENRE_TEMPLATE` (model-facing prompt text,
94
+ interpolated verbatim into the authoring prompt) was missing from `src/
95
+ promptregisters.js`'s `PROMPT_REGISTERS` inventory entirely.** Added; no other prompt content
96
+ found in that file. `docs/product/PRD.md`'s item 33 close-out box ticked.
97
+ - **F182 — the interview never waited for the install, so "Run it now?" was unreachable for a
98
+ repo that needs packages.** `scripts/run-interview.mjs` printed the install-gap message right
99
+ after `prepareSource` and fell straight through to the class questions, only re-checking
100
+ `missingDependencies` once, at hand-off — too late for the offer to ever fire. It now pauses
101
+ right there and loops on the same check (`"Press Enter once it has finished to check again, or
102
+ type skip to carry on without it: "`), printing `"packages found — carrying on."` on a
103
+ resolved recheck; `skip` or end of input carries on unresolved, same as before. Also drops the
104
+ misleading "then rerun this command" wording, which pointed at restarting the whole interview
105
+ when only the install is needed. Not yet proven live.
106
+ - **F181 — a key with an embedded newline (or other control character, or stray whitespace)
107
+ read as "set" by every presence-only key check and then crashed Node's own header-encode
108
+ (`ERR_INVALID_CHAR`) inside the paid span, rather than refusing at $0.** One exported pure
109
+ helper, `apiKeyProblem(value)` (`src/providers.js`, beside the provider table), reports a
110
+ plain-English reason (or `null`) for a value carrying CR, LF, TAB, another C0/DEL control
111
+ character, or leading/trailing whitespace — it never trims or repairs. Wired at every door
112
+ that already presence-checks a key: `scripts/run-author.mjs` (worker and judge keys),
113
+ `scripts/run-u.mjs` (worker and judge keys), `src/cli.js`'s `bareloop run`
114
+ (`ANTHROPIC_API_KEY`), and `scripts/run-interview.mjs`'s `KEYED` offer gate (a malformed key
115
+ no longer counts as keyed). Every refusal names the env var and the reason class, never the
116
+ value. Not yet proven live.
117
+ - **F187 — `scripts/run-u.mjs`'s printed invocation hints (the "approve and run" line and the
118
+ sleep-inhibitor wrapper line, both built through the shared `invoke()` helper) always named
119
+ `ANTHROPIC_API_KEY`, regardless of the job's actual provider.** A job on `openai-api`
120
+ (DeepSeek) printed the wrong variable name to set. `invoke()` now reads `providerEntry.envKey`
121
+ — the same resolved name the real key check at launch already uses. Not yet proven live.
122
+ - **F186 — the printed BEHAVIOUR line (and any replay) counted tool calls from EVERY run that
123
+ ever touched the patient tree, not just the run being read, because the authoring scout's gate
124
+ audit is written directly into the patient tree and the tree's `.gitignore` denies `*.jsonl`,
125
+ so a cold reset never removes it.** Fixed at the source, never by filtering `runBehaviour` on
126
+ `run_id` (a single run legitimately spans several). `scripts/run-author.mjs` now archives its
127
+ own gate audit out of the patient tree the moment authoring ends (`archiveGateAudit()`, called
128
+ from `finally` and once more before the one `process.exit()` path that skips it).
129
+ `scripts/run-u.mjs` now moves any stale audit aside right after `coldReset`, before its own
130
+ Gate ever opens (`moveStaleGateAudit`, new export from `scripts/u-patient.mjs`) — never on the
131
+ resume branch, where the tree's audit is that same run's own prior leg. Not yet proven live.
132
+ - **F184 — a malformed tool-call round on the WORKER path (`src/planrun.js`'s Loop-path `ask()`)
133
+ ended an attempt with nothing on the spine naming it — indistinguishable from the model
134
+ genuinely producing no useful work.** `ask()` now reads bare-agent 0.43.0's own
135
+ `r.malformedToolCall` marker (present since BA-27) and emits a distinct `worker-malformed-tool-
136
+ call` record (`{phase, iteration, name, error}`, scrubbed and length-capped) — visibility only:
137
+ no retry, no change to strikes/ladder/attempt counting/verdict routing, all arbiter-adjacent and
138
+ out of scope. Not yet proven live.
139
+ - **F179 — a malformed tool-call JSON on the FIRST authoring call had no earlier sound
140
+ declaration to fall back to, so 318e066's provider-red stopped the run outright.** hamr's
141
+ 2026-09-15 ruling: retry, never repair. `withMalformedToolCallShim` (`src/authorflow.js`,
142
+ wired into `makeLoopGenerate`) is a delegate provider (`Object.create`, never a mutation of
143
+ the shared instance) whose `_request` strips a malformed tool call out of the raw response
144
+ BEFORE `generate()`'s own `JSON.parse` can throw on it, so the round comes back PRICED on
145
+ bare-agent's own real usage instead of crashing. `askStructured` reads the resulting round
146
+ through its EXISTING malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`, no new cap),
147
+ tagged with a new `malformed-tool-call-arguments` axis — checked BEFORE the accept path and
148
+ regardless of how many calls survived the strip, so a reply carrying a malformed call
149
+ alongside a valid one is never accepted either (follow-up fix, same day, review-caught).
150
+ Scoped to `makeLoopGenerate` only — the authoring declaration calls and the confirm turn; the
151
+ scout and the worker path build their own provider wiring and still only get 318e066's stop
152
+ (an open gap). Not yet proven live.
153
+ - **F179/F180 — bare-agent 0.42.0's OpenAIProvider could throw a raw SyntaxError mid-call (a
154
+ malformed tool-call `arguments` string, `JSON.parse` with no try/catch), crashing past
155
+ `askStructured` and discarding an already-sound, already-measured declaration while the
156
+ billed call went unbooked.** `askStructured` (`src/authorflow.js`) now wraps its `generate`
157
+ call in try/catch, tested against a new shared predicate, `callCasualty` (`src/text.js`,
158
+ beside `priceOf`): a `HaltError` still re-raises (a governance exit, never laundered); an
159
+ ETIMEDOUT/TimeoutError-shaped throw or a JSON-mentioning SyntaxError lands as the same typed
160
+ `providerError` the resolved-`{error}` path already produced, booked at `costUsd: null`
161
+ (honest-unknown, never a complete-looking floor); anything else still crashes.
162
+ `authorscout.js`'s `settled` seam, which already hand-caught its own narrower idle-timeout
163
+ class, now delegates to the same predicate. No JSON repair, no new retry. Not yet proven live
164
+ (run `mu2bjmed` is the live crash this fixes; the corroborating upstream ask is BA-27 in
165
+ `docs/product/UPSTREAM-ASKS.md`).
166
+ - **F178/F177 — a signed fence could name `.git` or `node_modules` directly (or reach a nested
167
+ `node_modules` through an otherwise-legal fence), letting writes there go invisibly to
168
+ `changedSet`.** `scopeContained` (`src/validate.js`, the one containment law `validateJob`
169
+ and `validatePlan` both go through) now refuses any fence/scope whose normalized prefix has a
170
+ whole path segment equal to `.git` or `node_modules` (hamr's ruling, 2026-09-14, option B) —
171
+ a look-alike like `.github`/`my.git`/`node_modules_util` stays admitted. The runtime belt for
172
+ a NESTED `node_modules` a legal fence can still reach uses `tools.denyArgPatterns`
173
+ (`FORBIDDEN_WRITE_SEGMENT_PATTERN`, `src/planrun.js`), scoped to `write`/`edit` only —
174
+ bareguard's `fs.deny` was tried first for `.git` and reverted: it blocks ALL fs actions,
175
+ reads included, which silently over-blocked (caught by the full gate's own drift test,
176
+ `tests/tools.test.js`); `fs.deny` is never used for either book now. Not yet proven live.
177
+ - **F177 — a repo source whose tracked `.gitignore` did not mention `node_modules` let
178
+ installed packages read as the worker's own writes.** `changedSet` (`src/kinds.js`) unions
179
+ the tracked diff with `git ls-files --others --exclude-standard`, so an unignored
180
+ `node_modules` installed into the copy (live-verified: 247 files on a real copy of pulselog)
181
+ read as changes the worker made. `prepareSource` now hides `node_modules` in the copy's own
182
+ PRIVATE `.git/info/exclude` (hamr's ruling, 2026-09-14, option A) — the source repo's tracked
183
+ files, `.gitignore` included, are never touched. Not yet proven live.
184
+ - **PRD item 33 close-out — `prepareSource` freezes tracked files only, so a JS/TS repo's
185
+ prepared copy never carried `node_modules`, and every close stage needing a tool (`tsc`, a
186
+ test runner) instrument-stopped with nothing telling the person why.** bareloop never runs an
187
+ install itself (hamr's ruling, 2026-09-14, option A): `missingDependencies` (`src/source.js`,
188
+ exported) is a pure, $0, JS/TS-only detector that names the exact install command.
189
+ `scripts/run-interview.mjs` prints it right after `prepareSource` succeeds and again at the
190
+ hand-off, where a still-open gap suppresses the "Run it now?" offer entirely.
191
+ `scripts/run-author.mjs` refuses `source-deps-missing` at $0, before the scout and before the
192
+ provider/key are even read. Not yet proven live end to end (the detector and the refusal
193
+ paths are each proven directly; a real `npm ci` against a real prepared copy of a real repo
194
+ confirmed the hidden git's seed stays clean when the source's `.gitignore` ignores
195
+ `node_modules` — see the build report for the gap this does NOT cover, a repo whose
196
+ `.gitignore` does not ignore `node_modules`).
197
+ - **PRD item 33 close-out (L17) — the authoring interview never admitted an endpoint, so a
198
+ DeepSeek draft (`--provider openai-api`) authored against api.openai.com instead.**
199
+ `scripts/run-interview.mjs` now takes an optional `--base-url <url>` flag (no default; absent
200
+ leaves the draft's `baseUrl` field out entirely, never `null`/`''`) and validates it through
201
+ the existing `validateJob` `baseUrl` rule at $0 before writing anything. The interview's
202
+ header, the hand-off command, and `scripts/run-author.mjs`'s own start banner/`author-start`
203
+ event now name the endpoint when one is set; the key hint still points at the provider's own
204
+ env var (`OPENAI_API_KEY` for a DeepSeek endpoint) since there is no separate
205
+ endpoint-specific key. Not yet proven live.
206
+ - **F176 — the revise ladder kept a later revision even when it measured worse than an earlier
207
+ one.** `authorClose` (`src/authorflow.js`) now tracks every MEASURED (validated + seed-read)
208
+ iteration and falls back to the NEWEST SOUND one — a real red/green verdict, never an
209
+ instrument-stop — when the last measured iteration is unsound and an earlier one is not.
210
+ `stop` is never changed; the swap is reported via a new `fellBack` return field, an
211
+ `onPhase('author-fallback', …)` event, and the terminal signing readout. Not yet proven live.
212
+ - **F175 — the confirm turn's own honestly-raised `questions` never reached
213
+ `accepted.openQuestions`.** `runConfirmTurn` now carries the model's own `questions` from the
214
+ plan being accepted into `accepted.openQuestions` on every accepting path (confirm, type-goal,
215
+ the round-2 "fix" terminal), and fixes a related ledger bug where round 1's "fix" text (the
216
+ answer to round 2's redraft) was carried forward even after round 2 was accepted outright. Not
217
+ yet proven live.
218
+ - **F175's open half — "Confirm" and "Type the goal yourself" could still accept a plan whose own
219
+ raised question was never answered, silently confirming a genuinely-missing answer away** (run
220
+ `mu0voeo4`; only the silent-drop half above was covered by the first F175 fix). hamr's
221
+ 2026-09-16 ruling (option a): `runConfirmTurn`'s `confirm`/`type-goal` paths now force each
222
+ raised question through a new `kind: 'answer'` step before returning — no extra model call, a
223
+ blank answer re-asks the same question, `null` abandons like every other `ask`. Answers land in
224
+ a new `accepted.answeredQuestions` (Q:/A: pairs), kept separate from `openQuestions` (which
225
+ keeps its "could not resolve" meaning and is honestly empty on these two paths now).
226
+ `confirmedBlock` renders the answers as binding, and the SIGNING PREPARED readout
227
+ (`scripts/author-readout.mjs`'s new `answeredQuestionLines`) prints them when non-empty. The
228
+ `fix` path is unchanged. Not yet proven live.
229
+
230
+ ## [0.26.0] — 2026-09-14
231
+
232
+ ### Added
233
+
234
+ - **Source and Destination replace `--patient` (PRD item 33 M3, ruling 2):**
235
+ `scripts/run-interview.mjs` now asks Source and Destination as its own first two interactive
236
+ questions, before the picked verdict class's frozen set — never a `--patient` flag. Each is
237
+ proven mechanically, $0, the moment it is answered: Source is checked against the machine and
238
+ (for a local directory) language-detected immediately; Destination's SHAPE depends on what
239
+ Source turned out to be (the same `looksLikeRepoSource` rule `prepareSource` itself uses to
240
+ route this, now exported from `src/source.js` rather than duplicated) — for a repo Source, the
241
+ answer IS the write fence and fills `draft.writeScope` directly (the separate "FENCE" question
242
+ is gone); for every other kind, it is re-asked in a loop until it names an absolute, writable
243
+ directory (`proveDestination`). The moment both are answered, the interview calls
244
+ `prepareSource` itself to freeze Source into a hidden scratch copy under a fresh `into` nested
245
+ inside `--out`, and everything downstream — the class's own questions, the job spec's
246
+ `description`, and the `--source` handed to `scripts/run-author.mjs` — reads that PREPARED
247
+ COPY, never the original again. A non-repo Source gets Source and Destination proven and
248
+ frozen and then an honest named stop: bareloop's check catalogue is code-genre only today
249
+ (ruling 7 → M4); the prepared copy stays on disk regardless. `scripts/run-author.mjs` gains
250
+ `--source <tree>`, replacing `--patient <repoPath>`: it must be a tree a source door already
251
+ prepared (`readSourceManifest(dirname(source))` finds a manifest of kind `'repo'` beside it) —
252
+ an unprepared path dies loud naming the exact `scripts/prep-source.mjs` command to run first,
253
+ and a prepared non-repo source gets the same honest M4 stop, now recorded to the spine as
254
+ counted demand. `scripts/prep-source.mjs`'s printed next-step command follows suit.
255
+
256
+ - **A Source inside a repo (e.g. a monorepo package) is now a repo job (hamr's ruling A, PRD
257
+ item 34 loose end):** `looksLikeRepoSource`/`prepareSource` (`src/source.js`) used to treat a
258
+ folder as a repo source only when `.git` sat directly inside it, so a Source like
259
+ `myrepo/packages/api` fell to the plain-folder path (no checks yet, M4) and could hit an
260
+ untracked `node_modules` symlink as a refusal. A new `nearestGitAncestor(startDir)` walks up
261
+ from a directory to the nearest ancestor carrying a `.git` entry (a real repo directory, or a
262
+ linked-worktree/submodule file) — the one walk now shared by `looksLikeRepoSource`,
263
+ `prepareSource`'s own routing, and `src/detectlang.js`'s boundary walk, replacing three
264
+ separate copies. `prepareSource`'s directory branch freezes the WHOLE found repo's tracked
265
+ files when Source is a subfolder, exactly as it already did for a repo-root Source; the
266
+ manifest gains a `sourceSubdir` field (Source's path relative to the repo root, `''` at the
267
+ root) for a later piece to wire into scope — nothing reads it yet. Edge case, reported not
268
+ solved: a Source pointed at a deep folder under a large repo (e.g. one's whole home directory)
269
+ now freezes that WHOLE repo's tracked files, with no size or depth limit to catch it.
270
+
271
+ - **Language auto-detection (PRD item 33 M3, ruling 3):** `src/detectlang.js`'s `detectLanguage`
272
+ reads a code job's language off its own repository — `package.json` → js (TypeScript
273
+ included), `pyproject.toml`/`setup.py` → python — walking up from the Source folder to the
274
+ nearest ancestor manifest, never past the repository root (the nearest `.git`). A known
275
+ manifest this catalogue has no genre data for yet (`go.mod`, `Cargo.toml`, `pom.xml`,
276
+ `build.gradle(.kts)`, `*.csproj`/`*.sln`, `composer.json`) is a named `language-unsupported`
277
+ stop — never a silent fallback to js — carrying a `Refusal` shaped exactly like
278
+ `src/authorjob.js`'s own (`REFUSAL_LIB`/`REFUSAL_CATEGORY`, `request-red`). Two manifests at
279
+ the same nearest level is a named `ambiguous` result listing both; no manifest anywhere is
280
+ `no-code-job`, which is NOT an error (a plain-folder job still gets the form; M4 gives it
281
+ checks). The whole known-language table is one data list, keyed against `GENRE_LANGUAGES`
282
+ (`src/authoring.js`) — a later language landing there (M3b) needs no new code path here.
283
+
284
+ - **The question sets become the unified intake form (PRD item 33 M3 piece 3, signed ruling
285
+ "The intake form (RULED)").** `src/authorflow.js` reshapes the green/soft-green/hitl question
286
+ sets to the ruled 6-field form: Source and Destination (already asked as mechanical fields
287
+ since M3 piece 2) plus a free-text trio, numbered contiguously 1-3, whose text is now the
288
+ signed table's own "Holds" column, VERBATIM: Goal ("what you want to achieve"), What success
289
+ looks like ("checks a machine can count"), and Guardrails ("what must not happen or change" —
290
+ the old "must not change" and "worse than before" questions JOINED into one field; "worse than
291
+ before" is gone from the form entirely, moving to the confirm turn, piece 4, repo-only — now
292
+ built, see below). Soft-green adds a fourth field, Judge Examples ("one pass, one fail, and why" — the
293
+ old Q6 is retired as its own question, since the "why" half of a real pass/fail pair is what
294
+ `cardauthor.js`'s rubric compile already read it for). Each field's LABEL (Goal / What success
295
+ looks like / Guardrails / Judge examples) is shown for the first time too — a new `labelsFor`
296
+ reader alongside `questionsFor`/`requiredAnswersFor`, and the composer's own interview block
297
+ now shows the label with each answer. hitl (never shown, kept in code) renumbers the same way,
298
+ with no signed-table label for its own fourth question. Source and Destination's own wording
299
+ now also lives in `src/authorflow.js` (`SOURCE_FIELD`, `DESTINATION_FIELD_REPO`,
300
+ `DESTINATION_FIELD_PLAIN`, `destinationFieldFor`), so `scripts/run-interview.mjs` prints
301
+ library text for all six fields, none of its own. What the old Q2 used to tell the composer
302
+ (which files change, which are read-only) now reaches `authorPrompt` through a new optional
303
+ `writeScope` parameter (`writeScopeBlock`), fed from Destination's own proven write-scope
304
+ fence end to end (`authorClose` → `authorCloseForJob` → `scripts/run-author.mjs`'s
305
+ `draft.writeScope`) rather than silently vanishing.
306
+
307
+ - **The confirm turn (PRD item 33 M3 piece 4).** After the survey, `scripts/run-author.mjs`
308
+ now runs a paid, interactive confirm turn (`runConfirmTurn`, `src/authorjob.js` →
309
+ `src/authorflow.js`) before authoring: a $0 half (repo-only "worse than before", an
310
+ ambiguous-language pick, both asked BEFORE the scout) followed by up to 2 paid rounds where
311
+ the model drafts a plan — every check it will compose, the always-on guards as PROTECTIONS
312
+ (never named in the goal), and one signed goal sentence — and the person confirms it, fixes
313
+ it, types the goal themselves, or starts over. No hand-authored matcher ever compares the
314
+ goal against the checks (ruling 6) — the model states its own plan and the person is the one
315
+ judgement that accepts it. `scripts/run-interview.mjs` asks no separate goal question any
316
+ more (ruling 5's addendum, D2 = option B) and no longer dies on an ambiguous-language Source
317
+ (it defers the pick to the confirm turn, D7); `run-author.mjs`'s own ambiguous-language die()
318
+ is likewise gone. The confirm turn's own cost book absorbs the scout's prior spend (and its
319
+ own `costUsd: null` calls) before its first `capStop()` check, so a scout that already spent
320
+ most of the ceiling is visible to it immediately — only the confirm turn's OWN new calls
321
+ travel onward to `authorClose` as `priorCalls`/`priorRaws`, never the scout's a second time.
322
+ `run-author.mjs` is now interactive: a readline `ask` seam (same idiom as
323
+ `run-interview.mjs`'s own), closed in a `finally` around the whole paid span. The confirm
324
+ turn's open questions (a round-2 "fix" passed to the composer verbatim) print at the SIGNING
325
+ PREPARED readout (`openQuestionLines`, `scripts/author-readout.mjs`) — the signed spec format
326
+ itself is unchanged. `authorClose` gains a `confirmed` plan fed into `authorPrompt`'s new
327
+ `confirmedBlock` ("compose these checks and no other"). Every existing caller of
328
+ `authorCloseForJob`/`authorClose`/`authorPrompt` that passes no `ask` runs byte-identical to
329
+ before this piece existed. **`TYPES_GENRE_TEMPLATE` now composes its WORK stages only for a
330
+ confirmed check (D6 = A, "replace it", item 33 step S7):** the frozen TYPES genre template's
331
+ old unconditional work-stage skeleton (`typecheck`, `typecheck-outside`, `tests-kept`,
332
+ `suite-green`) and unconditional STRICT-checker mandate conflicted with ruling 6 ("a genre
333
+ never adds a check the goal did not ask for") for exactly the case run mtv8jihy surfaced (an
334
+ unasked `tsc --strict` stage). The STRICT form of the checker, and the WORK stages themselves,
335
+ are now composed only when a confirmed check — or, absent a confirm turn, the goal and
336
+ interview answers — asks for them; the GUARDS (`changed-from-seed`, `no-suppressions`) stay
337
+ always on, unaffected, since they are enforced structurally (`classGuards` +
338
+ `validateDeclaration`'s guards-absent red), never by this prompt text. The template is prompt
339
+ text handed to the authoring model only — never hashed into a signed spec, `closeDecl`, bundle
340
+ manifest, or bench row signature — so this edit changes nothing about an already-signed close.
341
+ `tests/authoring.test.js`'s byte-identity test is re-pinned to the new frozen block.
342
+ `scripts/run-interview.mjs` asks no separate GOAL question any more (ruling 5's
343
+ addendum, D2 = option B) and no longer dies on an ambiguous-language Source (D7 defers the
344
+ pick to the confirm turn); its own $0 validator pass filters both `goal`
345
+ (`CONFIRM_AUTHORED_FIELDS`) and, for a plain-folder job, `writeScope`
346
+ (`PLAIN_FOLDER_DEFERRED_FIELDS`, both `src/authorjob.js`) reds the same way it already filters
347
+ `AUTHORED_SPEC_FIELDS`'s. **A plain-folder Source no longer stops the interview at all (PRD
348
+ item 33 M3 piece 4, step S6, D5 = A ruling):** the form continues — class questions, job name,
349
+ budget, wall, and the offer — and the draft is written with no `writeScope` (Destination there
350
+ is an output directory, never a fence). The honest "no checks yet" stop (ruling 7 → M4) moves
351
+ to `run-author.mjs`, which now runs a plain-folder job's confirm turn too — over a $0, no-git
352
+ listing of the frozen tree rather than a scout (D5: the scout's register is code-only) — before
353
+ giving that same stop. If the confirm turn itself does not reach a signed plan, that stop
354
+ stands instead; the non-code-source stop only fires once a person actually confirmed one.
355
+
356
+ ### Removed
357
+
358
+ - **BREAKING: soft-green's rubric-card/calibration answer keys change shape (PRD item 33 M3
359
+ piece 3).** The old two-question pair (`answers[6]`, `answers[7]`) is now ONE answer at key
360
+ `4` (`SOFTGREEN_JUDGE_EXAMPLES_KEY`) — `src/cardauthor.js`'s `cardCasesPrompt` reads that one
361
+ key instead. Any hand-authored `answers.json` from before this change needs its keys
362
+ reshaped: green's old five (1-5) become three (1-3, Guardrails absorbing the old 3 and 5);
363
+ soft-green's old seven (1-7) become four (1-4).
364
+ - **BREAKING: `--patient` is gone from `scripts/run-interview.mjs` and `scripts/run-author.mjs`
365
+ (PRD item 33 M3, ruling 2).** `run-interview.mjs` asks Source (and Destination) as its own
366
+ first questions instead; `run-author.mjs` takes `--source <tree>`, which must be a prepared
367
+ copy (see "Added", above). Passing `--patient` to either script now is a loud, explicit
368
+ refusal naming its replacement, rather than a silently-ignored or misrouted flag.
369
+ - **BREAKING: `--lang` is gone from `scripts/run-interview.mjs` and `scripts/run-author.mjs`.**
370
+ Language is now auto-detected from Source's own repository (see "Added", above) — it is
371
+ a fact of the repo, never a flag a person sets. Passing `--lang` now is a loud, explicit
372
+ refusal naming the flag and why, rather than a silently-ignored no-op.
373
+
374
+ ### Fixed
375
+
376
+ - **The confirm turn's "protections" now come from code, never the model (item 34 loose-end
377
+ fix, finding #1 from live run mu0voeo4):** the confirm turn used to display whatever prose the
378
+ model invented for `protections`, and one live run invented a "behavior-preservation guard"
379
+ this build has no way to check (no kind in the catalogue can verify runtime behaviour). The
380
+ model is no longer asked for `protections` at all — the schema dropped the field — and what a
381
+ person sees and what gets recorded as `accepted.protections` is now rendered from the SAME
382
+ guards `classGuards` will actually compose (`src/authoring.js`'s `MECHANICAL_GUARDS`), through
383
+ a frozen plain-English line per guard (`GUARD_DESCRIPTIONS`, `src/authorflow.js`), plus the
384
+ write fence when one is set. The model instead returns `notChecked` — anything the person asked
385
+ for that no listed check can verify — shown under its own heading in the menu and in the
386
+ composer's confirmed-plan block, never silently dropped.
387
+ - **`run-interview.mjs`'s paid-step hand-off no longer describes a scout a plain folder
388
+ never gets (item 34 loose-end fix):** since 21561cf (D5=A), a non-repo Source gets no
389
+ scout — the confirm turn reads the file list and `run-author.mjs` stops at the M4 wall
390
+ ("no checks for this kind of job yet") before signing is ever reached. The hand-off text
391
+ printed the repo-only wording ("a real scout over that repository", "It stops at
392
+ prepareSigning") for every source regardless. A plain-folder session now prints its own
393
+ line naming the confirm turn and the M4 stop; a repo session's wording is unchanged.
394
+ - **`run-author.mjs`'s `language-unsupported` stop is now counted admission demand (item 34
395
+ loose-end fix on M3):** the check used to print the refusal and `process.exit(1)` before the
396
+ run's spine even existed, so nothing recorded it. The spine is now bootstrapped first (before
397
+ any provider is resolved, before the API key is read, before any spend) and the stop is
398
+ emitted through the same `refusalEvents()` channel every other refusal in the script uses,
399
+ so it folds into `src/ledger.js`'s `classifyIncidents` admission count instead of vanishing.
400
+ `run-interview.mjs`'s own printed line for the same stop no longer claims "the refusal IS the
401
+ record" — that script keeps no spine of its own, so it now says plainly that nothing here
402
+ recorded it, and points at `run-author.mjs` for the one that does.
403
+ - **Scan-then-freeze TOCTOU closed in the source front door (PRD item 34 L1):**
404
+ `prepareSource`'s folder/repo path used to hash+secret-scan each file's bytes once, then
405
+ re-read the same file a SECOND time off disk (`copyFile`) to freeze it into the hidden
406
+ tree — a window where a file changed on disk between the two reads would have its secret
407
+ scan run against different bytes than the ones actually frozen. The freeze step now
408
+ re-hashes its own (second) read and refuses (`source-changed-after-scan`) on any mismatch
409
+ against the scan-time hash, rather than caching every file's buffer (which would hold a
410
+ whole repo in memory at once — the streaming scan design exists to avoid exactly that).
411
+ `copyFile`'s mode-bit preservation is kept explicitly (`stat` + `chmod` on the frozen
412
+ file), verified not to regress.
413
+ - **A gitignored/untracked subfolder inside a repo no longer freezes into an empty
414
+ claim (hamr's ruling A, PRD item 34 loose end, 2026-09-13):** the ruling-2 addendum's
415
+ "a Source subfolder is a repo job" rework froze the WHOLE repo's tracked files, but
416
+ `listRepoFiles` only ever returns tracked paths — when Source itself is a subfolder git
417
+ tracks nothing under (`.gitignore`'d, or simply never committed), the freeze used to
418
+ succeed anyway with a `kind: 'repo'` tree holding none of Source's own files, silently
419
+ vanishing the person's input. `prepareSource` now refuses `source-untracked-in-repo`
420
+ before `into` is created — a repo-root Source, and a subfolder with at least one tracked
421
+ file underneath it, are unaffected.
422
+ - **The documented-exports guard now reads the doc instead of a hand-typed array (F167,
423
+ PRD item 34 L4):** `tests/index.test.js`'s "documented public surface" test used to check
424
+ a list of names copied out of `bareloop.context.md` by hand — the same class of bug that
425
+ let `datedDestination`/`pickDelivery` ship `undefined` in v0.25.0 while the guard stayed
426
+ green. `documentedExportNames` now parses the doc live (import-block examples, Public API
427
+ section headings, `Menus exported:` lists, and tight "X is/are exported" proximity) and
428
+ found two more real gaps the hand list missed: `cliMain` and (already fixed) the two
429
+ source-front-door helpers. Covers the doc's explicit export-marker idioms, not every
430
+ function mentioned in flowing prose — a broader sweep was tried and produced 21 false
431
+ positives out of 97 candidates on this doc, noise a regex cannot safely resolve.
432
+ - **The export list now equals the documented contract exactly, guarded both ways (PRD
433
+ item 34 L20):** the 91 undocumented names that were genuinely discussed in
434
+ `bareloop.context.md` prose (the "Close authoring", "Bundles", "THE REVIEW DOOR", "The
435
+ reuse ENVELOPE" and ledger sections — none of which used per-function `###` headings, so
436
+ the extractor's marker idioms could not credit any name in them) now carry an explicit
437
+ `import { ... } from 'bareloop'` marker next to their existing prose, including
438
+ `JUDGED_MARKER` (previously undocumented anywhere despite being required by every close
439
+ script this repo ships and by `exportBundle`'s `close-import-unexported` legality check).
440
+ `tests/index.test.js`'s guard is now a full two-way check — every export must be
441
+ documented and every documented name must be exported — replacing the one-directional
442
+ guard and the item-34-L4-scoped "37 restored" test, both now fully subsumed. Two
443
+ exclusions carry forward unchanged: `main` (the doc's pre-rename local name for the real
444
+ export `cliMain`) and `resumableOutcomes` (its own "exported" sentence describes "bundle",
445
+ not this name — the real export is `CHECKPOINT_OUTCOMES`).
446
+ - **The doc-reading extractor's own coverage gap closed (PRD item 34 L4 follow-up):** the
447
+ switch to reading `bareloop.context.md` live covered fewer names than the hand-typed array
448
+ it replaced — 37 exported names (`mintBridge`, `appendGreen`, `BRIDGE_SCHEMA`, the rest of
449
+ the reuse registry, plus `runStages`/`globToPrefix`/`scanSecrets`/three ledger helpers) sat
450
+ in flowing prose with no marker idiom the extractor reads, so the guard silently stopped
451
+ covering them. `bareloop.context.md` now carries an explicit marker (an `import { ... }
452
+ from 'bareloop'` example, or a tight "X is/are exported" sentence) for each of the 37, and
453
+ `tests/index.test.js` pins that they stay derived. A live count found 169 of
454
+ `src/index.js`'s 255 exports still undocumented by any marker idiom — reported, not fixed
455
+ here; a full reverse guard (every real export documented) was considered and rejected as
456
+ out of this fix's scope.
457
+ - **`SCOUT_LABEL` gains a third state in `scripts/run-u.mjs` (PRD item 34 L2):** an explicit
458
+ `--scout on` used to print the identical "scout ON (default)" text as no flag at all,
459
+ indistinguishable in printed logs/re-invocation lines — which matters for the
460
+ SCOUT-CONTRAST bench row's provenance. It now prints `scout ON (--scout on — operator
461
+ explicit)`, distinct from the true default and from `--scout off`'s unchanged label.
462
+ - **Job authoring is LLM-agnostic (PRD item 34 L17):** `scripts/run-author.mjs` no longer
463
+ forces `PROVIDER_NAME = 'anthropic-api'` for its scout and drafter — the identity is now
464
+ resolved from the operator's own draft (`resolveProvider(draft.provider)`), the same
465
+ no-flag/no-default rule `scripts/run-u.mjs` applies to the worker; a missing or
466
+ unrecognized draft provider dies loud, at $0, before any paid call, naming the known
467
+ table. `scripts/run-interview.mjs` gains a REQUIRED `--provider` flag with no default (a
468
+ default would silently re-lock every interview onto one vendor) and writes it into the
469
+ draft; the paid-step offer's key line and unset-key notice now name whichever provider's
470
+ own env var applies, never a hardcoded `ANTHROPIC_API_KEY`. Non-Anthropic drafting is
471
+ legal as of this change but not yet proven live — that is M3's DeepSeek proof run, not
472
+ this one.
473
+
474
+ ### Docs
475
+
476
+ - **`npx @scope/pkg`-shaped close commands documented as a KNOWN, KEPT limit (PRD item 34
477
+ L3):** `closeScriptCandidateToken`'s path-shaped test (`src/validate.js`) reads a scoped
478
+ package name as a script path because it contains `/`, so such a close can never be
479
+ signed — `bareloop.context.md` now says so next to the scope note it belongs beside. Not
480
+ fixed: it fails safe and no shipped job uses the shape.
481
+
482
+ ### Changed
483
+
484
+ - **DeepSeek secondary model swap (F171, PRD item 34 L16):** `deepseek-chat` is no longer
485
+ served by DeepSeek's API (a live `/models` call returned only `deepseek-flash` and
486
+ `deepseek-v4-pro`). The `openai-api` tier table (`src/providers.js`) and its per-model
487
+ `legacyMaxTokens` request-key gating now name `deepseek-flash`. F149's gating measurement
488
+ was taken on `deepseek-chat` and is carried forward UNVERIFIED on `deepseek-flash`;
489
+ pricing stays the estimated default (F113 unchanged).
490
+ - **`hitl` removed from every customer-facing surface (PRD item 34 L19):** `bareloop.context.md`,
491
+ and the usage/help text of `scripts/run-author.mjs` and `scripts/run-interview.mjs`, no
492
+ longer name `hitl` anywhere. The class, its terminals (`hitl-pause`, `hitl-decision-red`,
493
+ the legacy human-close terminal) and its `human-confirms` stage kind are untouched in code —
494
+ kept admitted for reuse, never revived, never shown to a customer again. The counted-demand
495
+ refusal a person sees when they pick an off-menu verdict class is unchanged (it names the
496
+ class and fwdloop by design). `tests/hitl-hidden.test.js` pins the doc and both scripts
497
+ case-insensitively hitl-free.
498
+
499
+ ### Removed
500
+
501
+ - **BREAKING: 78 undocumented names dropped from the `src/index.js` root export list (PRD
502
+ item 34 L20, owner's option A):** a live audit found 169 of 255 root exports had zero real
503
+ citation in `bareloop.context.md`; 91 of those were genuinely discussed in the doc's
504
+ prose and gained an explicit marker (see Fixed, above) instead of being dropped, but the
505
+ remaining 78 had no adopter-facing mention anywhere and no in-repo consumer reaching them
506
+ through the package root — a name imported directly from `import { x } from 'bareloop'`
507
+ and nothing else. Every dropped name's own module is untouched; only the root re-export
508
+ is gone, and each still exists inside its own module for the library's own internal use —
509
+ but `package.json`'s `exports` map only `"."` to `src/index.js`, so a package user can no
510
+ longer reach any of them, including via a submodule path such as `src/text.js`. Dropped:
511
+ `artifactHash`, `authorPrompt`,
512
+ `BASELINES`, `CALIBRATION_LABEL`, `cardCasesPrompt`, `CASE_VERDICTS`, `CATALOGUE_KINDS`,
513
+ `CATALOGUE_LIVE_KINDS`, `changedSet`, `classifySurvey`, `classMenu`, `CLASS_STATEMENTS`,
514
+ `CLOSE_DECL_FIELDS`, `closeGrade`, `compareExpectation`, `COMPILE_SYSTEM`,
515
+ `createReadShim`, `declarationSchema`, `DECLARED_CLOSE_CLASSES`, `DECLARED_GAP_KEEP`,
516
+ `DECLARED_GAP_PREFIX`, `DECLARED_GENRES`, `defaultJudgeLoop`, `DIRECTIONS`, `doorAgeGate`,
517
+ `doorRecordOf`, `EXIT_GREEN`, `EXIT_RED`, `EXIT_STOP`, `factsResist`, `GENRE`,
518
+ `GENRE_LANGUAGES`, `genreOwnedEnvNames`, `GREEN_QUESTIONS`, `guardNames`,
519
+ `HITL_DECISION_RED`, `HITL_PAUSE`, `HUMAN_CHECKPOINTS`, `INJECTION_CARD`,
520
+ `isDeclaredClose`, `JUDGED_FLOOR_KIND`, `JUDGE_MAX_TOKENS`, `LIVE_CLASSES`,
521
+ `LOCATE_AXES`, `LOCATE_LABEL`, `locatePrompt`, `makeLoopGenerate`, `makeSeedTrees`,
522
+ `MAX_PROPOSAL_RETRIES`, `MAX_STAGES`, `normalizeDeclaration`, `prepareWorkBranch`,
523
+ `PROPOSAL_LABEL`, `proposalSchema`, `proposalTool`, `PROPOSAL_TOOL_NAME`,
524
+ `QUARANTINED_CODE`, `QUESTION_SETS`, `RAW_TRIM_MARKER`, `READ_SHIM_ARMS`,
525
+ `readShimStrategy`, `REFUSAL_CATEGORY`, `REFUSAL_LIB`, `REVIEW_DOOR`, `seedFileList`,
526
+ `seedListing`, `shapeForkName`, `SOFTGREEN_QUESTIONS`, `SURVEY_CAUSES`, `TYPES_GENRE`,
527
+ `TYPES_GENRE_TEMPLATE`, `validateCard`, `validateFacts`, `validateJudgedArtifacts`,
528
+ `VERDICT_CLASSES`, `WORK_BRANCH_PREFIX`, `WORK_BRANCH_RE`, `wrapReadTool`. No in-repo
529
+ importer reached any of these through the package root (verified against every
530
+ `from '../src/index.js'` / `from './index.js'` / `from 'bareloop'` site in the tree), so
531
+ nothing else in this repo needed a path change.
532
+
8
533
  ## [0.25.1] — 2026-09-13
9
534
 
10
535
  ### Fixed