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 +247 -0
- package/README.md +1 -1
- package/bareloop.context.md +113 -29
- package/package.json +2 -2
- package/src/authorflow.js +24 -9
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/authorscout.js +83 -9
- package/src/calibrate.js +43 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +73 -21
- package/src/declaredclose.js +25 -11
- package/src/index.js +8 -5
- package/src/job.js +92 -2
- package/src/judged.js +240 -19
- package/src/kinds.js +89 -12
- package/src/planrun.js +194 -41
- package/src/providers.js +301 -0
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +9 -1
- package/src/run.js +68 -9
- package/src/transport.js +29 -12
- package/src/validate.js +4 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/authorscout.d.ts +27 -2
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +8 -1
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +130 -16
- package/types/kinds.d.ts +51 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +166 -0
- package/types/ratelimit.d.ts +59 -0
- package/types/run.d.ts +17 -3
- package/types/transport.d.ts +10 -4
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',
|
package/bareloop.context.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
608
|
-
is never a step knob
|
|
609
|
-
`
|
|
610
|
-
|
|
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
|
-
|
|
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
|
|
696
|
-
Because it is INSIDE the spec, `jobSpecHash` covers it by construction:
|
|
697
|
-
every signed judged spec hashes differently, the
|
|
698
|
-
|
|
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)
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
*
|
|
2934
|
-
|
|
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.
|
|
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.
|
|
29
|
+
"bare-agent": "^0.42.0",
|
|
30
30
|
"bareguard": "^0.13.0",
|
|
31
31
|
"litectx": "^0.32.0"
|
|
32
32
|
},
|