bareloop 0.22.0 → 0.24.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,253 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.24.0] — 2026-09-10
11
+
12
+ ### Added
13
+
14
+ - **`gemini-api` on the provider menu, ADMITTED-PENDING-PROBE** (PRD item 31.3;
15
+ hamr, 2026-09-09: *"anthropic, openai, gemini drop ollama for now"*). Reads
16
+ `GEMINI_API_KEY` and maps two genuinely different tiers — `sonnet` →
17
+ `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`. It has **zero runs**: the
18
+ probe rule (no endpoint in the menu without its own clean paid probe) is not
19
+ waived for it. `PROBE_STATUS` in `src/providers.js` records that debt per
20
+ provider and `probeWarningLines` renders the loud `⚠ UNPROVEN PROVIDER`
21
+ marker `scripts/run-u.mjs` prints at launch, right where money is about to be
22
+ spent. The table is read to WARN, never to REFUSE — refusing a provider hamr
23
+ admitted would be a second, invisible ruling.
24
+ - **Per-provider `endpointKey`** — each table entry declares the CONSTRUCTOR
25
+ option name it reads an endpoint from, instead of a hardcoded `baseUrl`.
26
+ bare-agent's providers do not agree on one spelling (`baseUrl` for
27
+ anthropic/openai/gemini, `url` for Ollama) and none of them validate unknown
28
+ option names, so a mis-spelled endpoint is silently DROPPED, not rejected:
29
+ measured live, `new OllamaProvider({baseUrl: X}).url` stays
30
+ `http://localhost:11434` with no error. That is the F149 class (DeepSeek
31
+ silently ignoring `max_completion_tokens`: asked 64 output tokens, returned
32
+ 665). A job spec still carries ONE field name, `baseUrl`; the translation to
33
+ each constructor's own spelling happens once, in `makeProvider`.
34
+
35
+ ### Changed
36
+
37
+ - **The judge is no longer a Claude judge** (PRD item 32; hamr, 2026-09-10:
38
+ *"what i care about is that judge becomes llm agnostic and not set to one
39
+ model or provider"*). `JUDGE_MODEL` is no longer read by any grading path.
40
+ The judge identity is RESOLVED per job by the new exported `resolveJudge`:
41
+ the spec's signed `judge: {provider, model}` if it names one, else **the
42
+ job's own worker provider and model** — so a DeepSeek or Gemini job judges on
43
+ its own provider and needs no second account. There is deliberately no
44
+ library fall-back: a run that cannot name its judge has not wired one, and
45
+ the judged stage instrument-STOPS rather than grading under an identity
46
+ nobody chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin so an
47
+ adopter's import survives. The judge is now built through the provider
48
+ factory like every other provider — no `new AnthropicProvider` survives for
49
+ a judge in `scripts/run-u.mjs`, `scripts/run-author.mjs` or `src/cli.js` —
50
+ and its key follows the resolved judge provider's own `envKey`, with
51
+ `JUDGE_API_KEY` as a role-named override in front.
52
+
53
+ **BREAKING for direct library callers.** `runCalibration`,
54
+ `foldJudgedArtifacts` and `prepareSigning`/`authorCloseForJob` now require
55
+ the resolved `judgeModel` and THROW without it; `runJob`/`runPlan` take a
56
+ `judgeModel` beside `judgeProvider`, and a judged stage stops as a wiring gap
57
+ when it is absent. Adding a default would be the silent fall-back this change
58
+ exists to remove.
59
+
60
+ **No threshold moved.** The judge still renders no verdict — it locates facts
61
+ and a deterministic `decide()` renders; unsure is RED; `CALIBRATION_SIZE` is
62
+ 10 with a 10-of-10 floor and no partial credit. Un-pinning a provider is not
63
+ a licence to move a bar. Self-grading (a worker's own family reading its
64
+ work) is licensed by MEASUREMENT: the calibration gate is exactly the
65
+ instrument that catches a judge which cannot see failures, and its frozen set
66
+ contains cases that MUST fail.
67
+ - **A new optional signed spec field, `judge: {provider, model}`** — both
68
+ halves required together, `provider` from the `PROVIDERS` menu, unknown
69
+ nested keys red. It is part of `jobSpecHash`, so changing the judge kills the
70
+ signature and forces recalibration — and the recalibration guard now fires
71
+ across a PROVIDER change, not just a model bump. The drafting agent cannot
72
+ express it: naming your own examiner is arbiter territory.
73
+ - **The judge's API key is demanded only when a judge is CALLED** (PRD item
74
+ 31.4). `scripts/run-u.mjs` hard-exited on a missing `ANTHROPIC_API_KEY` for
75
+ every job, so a `verdictType: 'green'` job with a DeepSeek or Gemini worker
76
+ and a mechanical close could not start without an Anthropic account it would
77
+ never spend a token against. Now the WORKER's key comes from the provider
78
+ table's own `envKey` and is always required; the JUDGE's key follows the
79
+ RESOLVED judge provider's own `envKey` (item 32.1 — the judge is no longer
80
+ pinned to Anthropic), with `JUDGE_API_KEY` as a role-named override in
81
+ front, and is required only when the close actually judges. When it does
82
+ refuse it refuses at $0, before the worker spends anything, naming the
83
+ judged-stage count and the resolved judge model — a judged run that
84
+ discovered this at the close would have paid in full for a verdict it
85
+ cannot render. A green `anthropic-api` job behaves byte-identically to
86
+ before. `bareloop run` (the bundle runner) is unchanged: `ANTHROPIC_API_KEY`
87
+ -only by a deliberate, documented contract.
88
+ - **One reading of "does this close judge?"** — `judgedStages()`/`closeJudges()`
89
+ are exported from `src/kinds.js` and the six open-coded copies of that
90
+ predicate now share them. The runner's own comment warned against "a second
91
+ reading of the declaration that can drift from the one the runner actually
92
+ executes"; 31.4 needed a seventh, to decide a REFUSAL, which is that warning
93
+ coming true.
94
+
95
+ - **`hitl` leaves the authoring menu; its code stays** (PRD item 31.1; hamr:
96
+ *"retire hitl from list, keep its code"*). Three distinct class lists replace
97
+ two: `LOCKED_CLASSES` (no guard battery exists — empty today),
98
+ `UNLISTED_CLASSES` (`['hitl']` — built, validates and RUNS, simply never
99
+ offered) and `MENU_CLASSES` (live minus unlisted). Only the AUTHORING surface
100
+ reads them, which is what keeps hitl's code alive and under test; locking the
101
+ class instead would have taken `validateCloseDecl`, the guard battery and two
102
+ whole test files with it. `scripts/run-interview.mjs` no longer advertises
103
+ `--verdict hitl` in a usage line the library then refuses.
104
+ - **Green-only prose corrected** (PRD item 31.2). `soft-green` was never
105
+ locked; the source comment claiming *"v1 STILL ADMITS ONLY green"* was false
106
+ and had already sent one build in the wrong direction, and
107
+ `bareloop.context.md` — the file that SHIPS — said all three classes were
108
+ admitted. Fixed, and pinned by tests, because prose drifts when nothing
109
+ executes it.
110
+
111
+ ### Security
112
+
113
+ - **`SECRET_PATTERNS` learns the Gemini/Google API key shape** (PRD item
114
+ 31.3, F160). `gemini-api` was admitted to real runs (this file, above)
115
+ without its own key shape joining the ONE secret-shape inventory
116
+ (`src/validate.js`) that drives `scanSecrets`, `redactSecrets` and
117
+ `sweepSecretLiterals` — a `GEMINI_API_KEY`-shaped literal (`AIza` + 35
118
+ chars) reaching a spine, a close's output, or a signed doc would have gone
119
+ undetected and unredacted. Fixed with a left-bounded pattern in the same
120
+ style as the existing shapes; monotonic (detection only added, nothing
121
+ loosened).
122
+
123
+ ### Notes
124
+
125
+ - Ollama is deliberately NOT admitted. It takes no key and bills nothing, so
126
+ every round would price at $0 through machinery that treats $0 as a real
127
+ price — the honesty violation the `?? 0` rule exists to stop. That question
128
+ gets answered before a menu entry, not after.
129
+
130
+ ## [0.23.0] — 2026-09-09
131
+
132
+ ### Added
133
+
134
+ - **`openai-api` on the provider menu, behind a provider FACTORY** (PRD item 28,
135
+ F149/F150/F156/F157). `src/providers.js` holds one table — provider name →
136
+ constructor, env key, tier models, per-model request-key gating — and both
137
+ runners go through it instead of naming `AnthropicProvider` directly. An
138
+ unknown provider name THROWS; there is no silent default. `openai-api` takes
139
+ an optional validated job-level `baseUrl` (https, or http on loopback only)
140
+ and admits ONE model, `deepseek-chat`, which earned the slot with a real paid
141
+ green through the shipped runner (run `mtu12vks`: green, $1.64 of $4, 10.3 of
142
+ 30 min). DeepSeek silently ignores `max_completion_tokens`, so its table entry
143
+ sets bare-agent's `legacyMaxTokens` — an output cap that does not bind is a
144
+ money hazard. The judge stays PINNED to `anthropic-api` and `JUDGE_MODEL`
145
+ whatever the worker's provider is. `bareloop run` (the bundle runner) is
146
+ `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would have
147
+ needed, rather than building another provider with the wrong key.
148
+ - **`provider` and `baseUrl` on the `job-start` spine record** (F157),
149
+ report-only. Without them an archive cannot separate anthropic rows from
150
+ `openai-api` rows, and every pooled cost or duration figure would be a
151
+ contaminated aggregate.
152
+ - **`stopReason` on every `worker-round`** (BA-13, report-only, null when
153
+ absent, never invented).
154
+
155
+ - **`runner-drained` — a `beforeExit` backstop for the absent-`job-end` class**
156
+ (PRD item 28's ruling (c); F140). A provider promise that never settles
157
+ (BA-25's class) used to let node drain and exit with NO terminal record at
158
+ all — no escalation, no outcome, no way to tell a silent drain from an
159
+ operator kill (F140: 10-of-235 archived spines with no `job-end`, one real
160
+ silent death). bare-agent 0.42 closed the known drop cases (F141), but the
161
+ class itself is "an instrument fires on absence," not a specific bug, so
162
+ the backstop stays needed. `runJob` (`src/run.js`) now registers ONE
163
+ `process.once('beforeExit', …)` right after `job-start`, tracked via a
164
+ local `ended` flag every `job-end` site sets; if the process still drains
165
+ with no `job-end`, it mints one with outcome `runner-drained`,
166
+ `spendComplete:false` (a drained run's in-flight spend is unknowable — the
167
+ floor already banked is honest, F6), and sets `process.exitCode` non-zero
168
+ (never `process.exit()`). The listener is removed in a `finally` so a
169
+ normal run never leaks it. `runner-drained` is a new terminal outcome,
170
+ documented in `bareloop.context.md`'s outcomes list — deliberately **not**
171
+ added to the resumable/checkpoint set (`CHECKPOINT_OUTCOMES`, `src/reuse.js`):
172
+ an unknown in-flight state is not a known-safe resume point.
173
+
174
+ - **Bounded 429 retry — a FALLBACK, not the primary answer** (PRD item 28's
175
+ parked a/b ruling, hamr verbatim: "do a/b as a fallback, and verify/validate
176
+ + no regression"; F143). A vendor HTTP 429 used to end the whole run as
177
+ `provider-red`, discarding all completed work, even though the vendor's own
178
+ response states exactly how long to wait. `src/ratelimit.js` (new,
179
+ `transport.js`'s sibling) adds ONE bounded retry that honours that stated
180
+ delay, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS`) — a wait longer than that is
181
+ not honoured at all. Wired into `src/planrun.js`'s provider-retry seam
182
+ (`withTransportRetry` renamed `withProviderRetries`) as a SECOND,
183
+ independent one-shot budget alongside the existing F115 transport retry;
184
+ the wall always wins (no retry past the deadline); each retry emits a
185
+ report-only `rate-limit-retry` spine record and does NOT floor
186
+ `spendComplete` (a 429 is a refusal — nothing was billed — unlike a
187
+ transport throw). **The primary answer stays the primary answer**: picking
188
+ a worker model with adequate rate headroom (F139/F143's measured TPM
189
+ table) — this retry only softens the failure mode when that was not
190
+ followed or headroom was still exceeded.
191
+
192
+ ### Changed
193
+
194
+ - Bumped `bare-agent` pin from `^0.39.0` to `^0.42.0`. 0.42.0 brings: BA-24 —
195
+ `OpenAIProvider` now sends `max_completion_tokens` by default (GPT-5-safe),
196
+ with a new `legacyMaxTokens` option to send the legacy `max_tokens` key
197
+ instead; BA-25 — a response body cut after headers (peer `'aborted'`,
198
+ stream `'error'`, or a premature `'close'` before `'end'`) now REJECTS
199
+ `generate()` with a retryable `ProviderError` (`context.bound:'transport'`)
200
+ instead of leaving the promise pending forever; a new `loop:truncated`
201
+ stream event plus `stopReason` on the `onLlmResult` metering payload; and
202
+ `options.toolChoice` on `OpenAIProvider`.
203
+
204
+ ### Fixed
205
+
206
+ - **F154** — every provider call bareloop makes now carries a deadline. Seven
207
+ sites had none: the softgreen judge, the calibration gate, the authoring
208
+ scout's three rounds, the declaration/revise boundary, and the bridge picker.
209
+ A silent endpoint HANGS the process (an open socket is an active handle, so
210
+ the drain backstop cannot see it); a deadline is the only instrument that
211
+ fires on the absence of events. Tighten-only.
212
+ - **F155** — the judged floor graded whatever LOCATE reported, so a silently
213
+ omitted function passed on the subset. `decide()` now diffs the artifact's own
214
+ top-level functions against the reported set and reds, naming what was
215
+ missing. The declaration shapes live ONCE (`FN_SHAPES`), from which both the
216
+ locate prompt's prose and the detector are derived.
217
+ - **F153** — a provider's own error sentence now reaches the operator instead of
218
+ a bare status number, redacted through the one secret inventory and capped.
219
+ - **F151** — the bounded 429 retry, added last release, fired on a real vendor
220
+ 429 for the first time: it parsed the stated wait from prose, waited it, and
221
+ retried once. Mechanism proven; on a TPM-saturated model the retry is not
222
+ expected to recover, and did not.
223
+ - **F147** — the F59 scout summary round (and the authoring scout's recovery
224
+ round, the same shape in `src/authorscout.js`) re-fed a transcript bare-agent
225
+ had already system-prepended into a new `Loop.run()`, which prepended the
226
+ persona AGAIN: two identical system messages at index 0/1. Real OpenAI
227
+ tolerates that; vLLM-class OpenAI-compatible backends reject it (HTTP 400
228
+ "System message must be at the beginning" — the Qwen death F146 left
229
+ unexplained). Both sites now strip the leading system message before
230
+ continuing, as bare-agent's own `Loop.chat()` does. Proven live: the captured
231
+ failing request goes 400 → 200 with one line removed; deepseek-chat then
232
+ greened a full job (F149/F150).
233
+ - **F141** — `src/transport.js`'s `isTransportFailure` now also recognizes
234
+ bare-agent's own BA-25 transport classification
235
+ (`err.context.bound === 'transport'`), after the existing HTTP-status and
236
+ `retryable:false` guards. Before this fix, the exact class hamr's F115
237
+ one-retry ruling names — a call that dies mid-read, no full response —
238
+ would have ended provider-red with no retry once the 0.42.0 pin landed,
239
+ because the new rejection shape carried none of the signals the classifier
240
+ previously looked for (no `status`, `retryable:true`, no matching error
241
+ code/message). Retry budget unchanged: `TRANSPORT_RETRIES = 1`.
242
+ - **Review door refuses `close-timing-red` instead of defaulting the close timeout**
243
+ (F137, `docs/product/CLOSE-INTEGRITY-BUILD.md` "Ruled 2026-09-07", found by
244
+ orchestrator review of M3, run `mtqwmb9l`). `scripts/run-u.mjs`'s `accept` door path
245
+ resolved the mechanical re-run's close timeout via `resolveCloseTimeoutMs` and, when
246
+ that timing pass itself timed out, silently fell through to `closeTimeoutMs:
247
+ undefined` — the library's 120s default — substituting an unauthorized ceiling for a
248
+ door decision, the exact second silent-default class M3 was built to remove one door
249
+ up. A door whose own timing pass times out now REFUSES outright as a named
250
+ `close-timing-red` door stop (same wording/options as the in-run escalation, new pure
251
+ renderer `doorTimingRedLines` in `scripts/u-readout.mjs`): `answerReviewDoor` is never
252
+ called, nothing is recorded (no door record), nothing is released, nothing is spent,
253
+ and the process exits non-zero.
254
+
8
255
  ## [0.22.0] — 2026-09-07
9
256
 
10
257
  ### Added
package/README.md CHANGED
@@ -82,7 +82,7 @@ A signed job spec goes in. A verdict comes out.
82
82
 
83
83
  ```js
84
84
  import { runJob, jobSpecHash, makeSpine } from 'bareloop';
85
- import { AnthropicProvider } from 'bare-agent';
85
+ import { AnthropicProvider } from 'bare-agent/providers';
86
86
 
87
87
  const spec = {
88
88
  schema: 'job-v1',
@@ -70,6 +70,39 @@ inherited rule carries the green that minted it and the contrast that attributed
70
70
  with `anthropic-api` on cost, and budgets don't transfer between them.
71
71
  `job.provider === 'clipipe-subscription'` with no `nativeProvider` supplied is
72
72
  `interpreter-red`, never a silent fall-back to the metered API (`src/planrun.js:1073`).
73
+ `provider: 'openai-api'` (PRD item 28) takes the ordinary `Loop` path against an
74
+ OpenAI-shaped endpoint, reads `OPENAI_API_KEY`, and accepts an optional job-level
75
+ `baseUrl` (https, or http on loopback only; credentials in the URL are a red). Its one
76
+ admitted model is `deepseek-chat`, which earned the slot with a real paid green — no
77
+ endpoint or model enters the menu without its own clean paid probe. The provider table
78
+ lives in `src/providers.js` (`resolveProvider`/`makeProvider`/`buildRunnerProviders`);
79
+ an unknown provider name THROWS there, it never falls back to a default. Per-model
80
+ request-key gating lives in that table: `deepseek-chat` sets bare-agent's
81
+ `legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
82
+ output cap that does not bind is a money hazard. `provider: 'gemini-api'` (PRD item 31.3)
83
+ takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
84
+ (`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
85
+ **ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
86
+ `PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
87
+ `probeWarningLines(provider)` renders the loud launch-time marker `scripts/run-u.mjs`
88
+ prints. That table is read to WARN, never to REFUSE. Each entry also declares its own
89
+ `endpointKey` — the CONSTRUCTOR option name it reads an endpoint from — because
90
+ bare-agent's providers do not agree on one spelling (`baseUrl` for anthropic/openai/
91
+ gemini, `url` for Ollama) and none of them validate unknown option names, so a
92
+ mis-spelled endpoint is silently DROPPED rather than rejected. Your job spec keeps one
93
+ field name, `baseUrl`; the translation happens once, inside `makeProvider`. Ollama is
94
+ deliberately NOT admitted: it takes no key and bills nothing, so every round would price
95
+ at $0 through machinery that treats $0 as a real price. The JUDGE IS NO LONGER PINNED
96
+ (PRD item 32): `resolveJudge` (exported) resolves it per job — the spec's signed
97
+ `judge: {provider, model}` if it names one, else **this job's own worker provider and
98
+ model**, so a DeepSeek or Gemini job judges on its own provider and needs no second
99
+ account. There is no library fall-back: a run that cannot name its judge has not wired
100
+ one, and the judged stage instrument-STOPS rather than grading under an identity nobody
101
+ chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin — no grading path reads it.
102
+ The judge's key follows the resolved judge provider's own `envKey`, with `JUDGE_API_KEY`
103
+ as a role-named override in front of it. The bundle runner
104
+ (`bareloop run`) is `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would
105
+ have needed, rather than constructing another provider with the wrong key.
73
106
  - **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
74
107
  `bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
75
108
  ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
@@ -85,7 +118,7 @@ inherited rule carries the green that minted it and the contrast that attributed
85
118
 
86
119
  ```js
87
120
  import { runJob, jobSpecHash, makeSpine } from 'bareloop';
88
- import { AnthropicProvider } from 'bare-agent';
121
+ import { AnthropicProvider } from 'bare-agent/providers';
89
122
 
90
123
  const spec = {
91
124
  schema: 'job-v1', job: 'my-maintainer',
@@ -130,13 +163,14 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
130
163
  |---|---|---|
131
164
  | `job` | kebab-case slug | |
132
165
  | `description` | non-empty string | |
133
- | `provider` | `anthropic-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
166
+ | `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-chat` today — 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` |
134
167
  | `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) |
135
168
  | `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
136
169
  | `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
137
170
  | `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 |
138
171
  | `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 |
139
- | `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 model stays library-pinned (`JUDGE_MODEL`) pending recalibration |
172
+ | `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 |
173
+ | `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 |
140
174
  | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
141
175
  | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
142
176
  | `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
@@ -147,7 +181,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
147
181
  | field | shape | notes |
148
182
  |---|---|---|
149
183
  | `goal` | non-empty text | what the agent plans against |
150
- | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three are ADMITTED today** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself. `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; `hitl` is LEGACY — retired as a design direction (see the direction note at the top) but still admitted and still works exactly as documented here. 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 |
184
+ | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three still VALIDATE and RUN** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself, and an existing spec of any class executes. **But AUTHORING offers two: `green` and `soft-green` (`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; `hitl` is RETIRED and UNLISTED (`UNLISTED_CLASSES`, PRD item 31.1) — the class moved to fwdloop, so `runInterview` refuses a hitl pick as counted demand and no new hitl job can be drafted, while its battery, its runtime and an already-signed hitl spec are untouched and still work exactly as documented here. The three lists are DIFFERENT facts: `LOCKED_CLASSES` (no battery — cannot validate or run; empty today), `UNLISTED_CLASSES` (built and runnable, never offered — `['hitl']`), `MENU_CLASSES` (what authoring offers). 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 |
151
185
  | `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; a `gold`/`rubric`/`hitl` object close still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
152
186
  | `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 |
153
187
  | `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) |
@@ -496,7 +530,7 @@ Escalations are decision-ready (category, options, spend); cap-halt is its own
496
530
  category, never merged with "wrong". A thrown middle is relayed by its `category`
497
531
  property (`cap-halt`, `gate-red`, …); an unnamed throw is `interpreter-red`. Close output
498
532
  is scrubbed at capture (an injected `redact`, wired to bareguard by `runPlan` with the
499
- validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox) so a
533
+ validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox/AIza) so a
500
534
  secret a checked command echoes never enters the append-only spine or a worker prompt —
501
535
  a benign gap is byte-identical (secrets hard line; design law #7 intact).
502
536
 
@@ -558,7 +592,7 @@ a step, or test it without a provider.
558
592
 
559
593
  | step | call | what it is |
560
594
  |---|---|---|
561
- | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), all three LIVE as of softgreen module 3 — nothing is locked today (`LOCKED_CLASSES` is empty) — and 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 two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the LEGACY `hitl` set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the `soft-green` set is likewise those five plus two more — Q6 the signed rubric card (what separates a pass from a fail) and Q7 the frozen calibration set (one example to pass, one to fail) — the judged floor's two required inputs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
595
+ | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), all three sets still EXIST, but the interview ADMITS only what is on the menu: nothing is locked today (`LOCKED_CLASSES` is empty) and `hitl` is UNLISTED (PRD item 31.1), so `runInterview({verdictType:'hitl'})` 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 two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the RETIRED, UNLISTED `hitl` set (still readable via `questionsFor`, never authorable) is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the `soft-green` set is likewise those five plus two more — Q6 the signed rubric card (what separates a pass from a fail) and Q7 the frozen calibration set (one example to pass, one to fail) — the judged floor's two required inputs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
562
596
  | 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 |
563
597
  | 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 |
564
598
  | 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 |
@@ -604,10 +638,13 @@ so it takes no `cmd`, no `timeoutMs` and no `env`; it is `offer: false` BY LAW
604
638
  (`NEVER_OFFERED_KINDS` — a paid judge is never an in-run ruler); and it SKIPS the seed-verdict
605
639
  read (ruling 8) as a recorded `skipped` row.
606
640
 
607
- **Wiring a judged close, and what it costs.** The judge is PINNED (`JUDGE_MODEL`, exported) and
608
- is never a step knob: the runner supplies `runJob({ judgeProvider })` — its own seam, not
609
- `provider`/`providerFor` — and a judged stage with none instrument-STOPS as a wiring gap rather
610
- than grading on whatever binding was at hand. Each locate call is metered per call and emitted
641
+ **Wiring a judged close, and what it costs.** The judge is RESOLVED per job (`resolveJudge`,
642
+ PRD item 32) and is never a step knob or agent-selectable — naming your own examiner is arbiter
643
+ territory, and `judge` is a field the human SIGNS into the hash. The runner supplies BOTH halves,
644
+ `runJob({ judgeProvider, judgeModel })` — its own seam, not `provider`/`providerFor` — and they
645
+ travel together: a judged stage missing EITHER instrument-STOPS as a wiring gap (two distinct
646
+ messages, identity checked first) rather than grading on whatever binding was at hand or under a
647
+ name nobody wrote. Each locate call is metered per call and emitted
611
648
  as a distinct `judge-round` spine record, which `runJob`'s ONE ledger accounts exactly like a
612
649
  `worker-round` (`ACCOUNTED_ROUND_TYPES`, and `readResume`'s fold reads that same list, so a
613
650
  resumed leg cannot silently widen a signed budget by the close's own spend). A budget funds the
@@ -677,8 +714,9 @@ null rather than an exact-looking floor.
677
714
  carrying a `judged-floor` stage is refused when no set is stored, when no `judgeLoop` seam is
678
715
  wired (a wiring gap, never a silent pass), when the gate is a casualty, and when anything short
679
716
  of all-of-them grades. It runs **after** gates 1–3, so no $0 refusal is ever paid for. The
680
- signing record keeps WHAT was certified — `cardHash`, `casesHash`, `setHash` (which covers
681
- `JUDGE_MODEL`), the graded rows and the battery — beside `judgeModel` itself.
717
+ signing record keeps WHAT was certified — `cardHash`, `casesHash`, `setHash` (which covers the
718
+ resolved judge identity), the graded rows and the battery — beside `judgeModel` itself.
719
+ `runCalibration` takes `judgeModel` as a REQUIRED argument and throws without one.
682
720
 
683
721
  **D9.3 for a judged-ONLY close** (ruling 3, hamr: *"fix it now, we are delivering softgreen"*).
684
722
  A judged stage skips the seed read (ruling 8), so a close whose only work stage is judged has no
@@ -692,10 +730,12 @@ bump forces a full recalibration"* used to be an operator-side rule with nothing
692
730
  which makes it prose (F45); it now has both halves of a detector. The tier the signer's set was
693
731
  graded by is stored beside the cases as **`calibration.judgeModel`** — `CALIBRATION_FIELDS`
694
732
  enumerates a stored set as exactly `cases` and `judgeModel` and nothing else — written by
695
- `foldJudgedArtifacts` from the PIN (`JUDGE_MODEL`) rather than from anything a caller may name.
696
- Because it is INSIDE the spec, `jobSpecHash` covers it by construction: bump the constant and
697
- every signed judged spec hashes differently, the signature dies, and the re-sign is what re-runs
698
- the calibration gate. **`validateCloseDecl` REQUIRES it** on any stored set (`missing-required`
733
+ `foldJudgedArtifacts` from the RESOLVED judge identity the caller hands in (required; it throws
734
+ without one). Because it is INSIDE the spec, `jobSpecHash` covers it by construction: change the
735
+ judge — its model OR its provider — and every signed judged spec hashes differently, the
736
+ signature dies, and the re-sign is what re-runs the calibration gate. `runJudgedFloor` refuses to
737
+ grade when the stored identity is not the one about to grade, naming both, and it does not
738
+ degrade in either direction. **`validateCloseDecl` REQUIRES it** on any stored set (`missing-required`
699
739
  on `…calibration.judgeModel`) — a set nobody can attribute to a tier is a floor nobody can tell
700
740
  apart from a bumped one. **`declaredStages` stamps it onto the judged stage** as
701
741
  `calibrationJudgeModel`, from the SIGNED bytes and through the one seam that already turns a
@@ -1008,7 +1048,18 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
1008
1048
  Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
1009
1049
  plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
1010
1050
  provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | hitl-pause |
1011
- hitl-decision-red | step-red:<id>`.
1051
+ hitl-decision-red | step-red:<id> | runner-drained`.
1052
+
1053
+ **`runner-drained` (F140/PRD item 28(c))** is a distinct class from every outcome above: it is
1054
+ NOT `runJob`'s own return value, it is a `beforeExit` backstop's terminal, emitted from outside
1055
+ the normal control flow the instant node drains with no `job-end` at all — a provider promise
1056
+ that never settled (BA-25's class; bare-agent 0.42.0 closed the known drop cases per F141, but
1057
+ "an instrument fires on absence" stays a permanent class, not a fixed bug). It means: exits
1058
+ non-zero, `spendComplete:false` always (a drained run's in-flight spend is unknowable — the
1059
+ floor already banked is honest, an exact-looking total would not be, F6). **It is deliberately
1060
+ NOT in the resumable/checkpoint set** (`CHECKPOINT_OUTCOMES`/`RESUMABLE_HALTS`, `src/reuse.js`)
1061
+ — an unknown in-flight state, unlike a clean `cap-halt`/`wall-halt`/`provider-red`, is not a
1062
+ known-safe resume point.
1012
1063
 
1013
1064
  **`readShim` (default `false`) — the capped read seam, per ARM.** The value names which levers
1014
1065
  run — the four arms of the frozen Phase 2 pre-registration:
@@ -1191,17 +1242,36 @@ round of this attempt being priced repairs nothing about the one before it); and
1191
1242
  reporting with the rest end of gate"). Every worker Loop (scout, drafter, each step, the fix
1192
1243
  worker — the ONE seam in `planrun.js`) gets exactly ONE extra attempt, and only for a
1193
1244
  transport-class throw — fetch itself throwing with no HTTP response at all (a TLS fault,
1194
- `ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause); an HTTP response
1195
- (4xx/5xx/429) is never retried here and rides bare-agent's own unchanged policy. The budget is
1196
- fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob, tighten-only
1197
- doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase, attempt, error,
1198
- recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw before any usage
1199
- figure came back, so **any** transport retry floors `spendComplete` for the rest of the run even
1200
- when the retry recovers and the run finishes green — the same one-way flag class as `stalled`
1201
- and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
1245
+ `ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause), OR (bare-agent
1246
+ 0.42.0, BA-25) a response body cut after headers but before it completed — the provider's own
1247
+ `guardResponseSettles` rejects that as a retryable `ProviderError` tagged
1248
+ `context.bound:'transport'`, which `isTransportFailure` also recognizes (F141). An HTTP response
1249
+ (4xx/5xx generally) is never retried here and rides bare-agent's own unchanged policy. The budget
1250
+ is fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob,
1251
+ tighten-only doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase,
1252
+ attempt, error, recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw
1253
+ before any usage figure came back, so **any** transport retry floors `spendComplete` for the rest
1254
+ of the run even when the retry recovers and the run finishes green — the same one-way flag class
1255
+ as `stalled` and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
1202
1256
  so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
1203
1257
  field presence and never has to launder a missing `spentUsd` into `$0`.
1204
1258
 
1259
+ A **rate-limit retry** (PRD item 28's parked a/b ruling, hamr verbatim: "do a/b as a fallback, and
1260
+ verify/validate + no regression"; F143/`src/ratelimit.js`) rides the SAME seam as a second,
1261
+ INDEPENDENT one-shot budget: an HTTP 429 gets ONE retry that honours the vendor's own stated
1262
+ wait, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS` — a longer stated wait is not honoured at all, no
1263
+ retry). This is a FALLBACK, not the primary answer — the primary answer (F139) is picking a
1264
+ worker model with adequate rate headroom; this only softens the failure mode when that was not
1265
+ enough. The wall always wins here too: before sleeping, a retry that would cost more than
1266
+ `clock.remainingMs()` is refused and the error rides straight through, exactly like the transport
1267
+ ladder's own wall carve-out. Each retry emits a report-only `rate-limit-retry` spine record
1268
+ (`{phase, attempt, statedMs, waitedMs, error, recovered}`, no cost field). Unlike the transport
1269
+ retry, a rate-limit retry does **not** floor `spendComplete`: a 429 is a REFUSAL — the vendor
1270
+ rejected the request before processing it, so nothing can have been billed (a stated assumption,
1271
+ not independently measured against a real provider). The two budgets are independent — a
1272
+ transport throw and a 429 may both occur, and both retry, across one call's lifetime, but each at
1273
+ most once.
1274
+
1205
1275
  **The plan flow (Layer 2).** `job-start` carries `shape: 'plan'` + the goal; plan steps are
1206
1276
  tool-mode by construction. The flow (`runPlan`, also exported for direct callers who own
1207
1277
  their own ledger): **close precheck** (`already-green` is a
@@ -1645,7 +1715,11 @@ that moved (`door-accept-red`) and a missing registry all come back as named red
1645
1715
  a person) before it is honoured: hamr's ruling that an accept is not a rubber stamp, and the
1646
1716
  answer to a tree that can move in the 60 days a door keeps. A red REFUSES the accept with the
1647
1717
  stage named and records **nothing**. A pass records the disposition and, over a held judged
1648
- green, RELEASES the credit through module 6's `applyDoorDecision`.
1718
+ green, RELEASES the credit through module 6's `applyDoorDecision`. `scripts/run-u.mjs` resolves
1719
+ the re-run's ceiling itself via `resolveCloseTimeoutMs` before calling this, and refuses outright
1720
+ (F137) as a named `close-timing-red` door stop — `answerReviewDoor` never called, nothing
1721
+ recorded — if that timing pass itself times out, rather than falling through to `runClose`'s
1722
+ library default.
1649
1723
  - **`rerun`** carries the person's words back as a directive (`next: 'rerun'`) and re-proves
1650
1724
  nothing: the new run's close is what judges. Empty or whitespace text is refused at the same
1651
1725
  seam every other door is.
@@ -2677,7 +2751,7 @@ hash and needs re-approval, exactly like any other semantic edit.
2677
2751
  | `close-sha-mismatch` | `exportBundle` (PRD item 27/M2) | a `close[].sha256` the spec carries does not match the bytes of the script actually being packed into the bundle — the two signatures (this one, and `bundleHash`'s manifest hash over the rewritten script) cover the same bytes by two different paths and must never drift apart silently (N4's own hazard, one layer up). Only fires when a value is PRESENT and wrong; a spec with no sha256 at all reds `missing-required` upstream, at `validateJob` |
2678
2752
  | `close-tampered` | `runPlan` (PRD item 27/M2) | a close script's bytes no longer match its signed `sha256` — checked at run start (alongside `close-absolute-path`, before the close-first precheck and before any provider call) AND before EVERY close run thereafter (`runCloseStages`'s wrapper in `src/planrun.js`, the one seam the precheck/preflight/check-passes/close-fix-loop all share). Distinct from `close-red` (a judged "no" from a working close) and from `close-crashed`/`close-timeout`/`close-killed` (the close ran and hit an instrument fault): this is decided BEFORE the close is even spawned, and it means the close that ran would not have been the one the operator approved. Registered in `CLOSE_FAULTS` (`src/ralph.js`) so it rides out through the same forbidden-zone machinery as every other close fault — never retried, never fed back as a gap |
2679
2753
  | `close-dir-required` | `runPlan` (PRD item 27/M3 Part B) | a close script's source mentions `BARELOOP_CLOSE_DIR` (it reads its own books directory) but the runner was given no `closeDir` at all — `checkCloseDirRequired` (`src/close-integrity.js`) reuses the same `readCloseScripts` reader the other two integrity checks share. $0, run start, before the close-first precheck and before any provider call |
2680
- | `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely) |
2754
+ | `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely). `scripts/run-u.mjs`'s review door hits the SAME red on its own `accept` timing pass (F137) — it refuses the door outright rather than calling `answerReviewDoor` at all, so nothing is recorded |
2681
2755
  | `wall-under-close-timeout` | `runPlan` (PRD item 27/M3) | `job.maxWallMs` is set and is under the EFFECTIVE per-stage close timeout just resolved (autoset or signed) — a budget under one close cannot fund its own close. `validateJob`'s own `MIN_WALL_MS` floor only bounds the un-autoset library default; this run-start check is the honest shape once the real ceiling is known, rather than a silent clamp |
2682
2756
  | `bundle-missing` | `readBundle` | `dir` is not a directory at all |
2683
2757
  | `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
@@ -2930,8 +3004,18 @@ blast radius.
2930
3004
 
2931
3005
  ## Gotchas
2932
3006
 
2933
- *TBD from real adopter friction; recorded here as they're found (repo-side friction goes
2934
- to `docs/logs/FINDINGS.md`).*
3007
+ *Recorded here as they're found (repo-side friction goes to `docs/logs/FINDINGS.md`).*
3008
+
3009
+ - **`non-green-verdict` is a misleading refusal verb, and it is kept on purpose.** When
3010
+ `runInterview` refuses a job for having no git repository (`repoPath` absent), the refusal
3011
+ carries `verb: 'non-green-verdict'` and `path: 'repoPath'`. The name is wrong: the refusal
3012
+ is about the GENRE — this authoring flow drafts code-genre closes against a git seed, and
3013
+ that is true of BOTH classes it offers — not about the verdict class, and it fires for a
3014
+ `soft-green` pick exactly as it does for `green`. **Read `path`, never `verb`, to know what
3015
+ was refused.** The verb is kept because it is the demand ledger's key for this refusal:
3016
+ renaming it would split one running count into two that read as different demands. Named
3017
+ rather than fixed (PRD item 31.2); say so if you would rather have the accurate name and
3018
+ accept the discontinuity.
2935
3019
 
2936
3020
  ## Constraints
2937
3021
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.22.0",
3
+ "version": "0.24.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",
@@ -26,7 +26,7 @@
26
26
  "prepublishOnly": "npm run build:types"
27
27
  },
28
28
  "dependencies": {
29
- "bare-agent": "^0.39.0",
29
+ "bare-agent": "^0.42.0",
30
30
  "bareguard": "^0.13.0",
31
31
  "litectx": "^0.32.0"
32
32
  },