bareloop 0.22.0 → 0.23.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 +125 -0
- package/README.md +1 -1
- package/bareloop.context.md +60 -13
- package/package.json +2 -2
- package/src/authorflow.js +7 -2
- package/src/authorscout.js +83 -9
- package/src/calibrate.js +10 -0
- package/src/cli.js +40 -19
- package/src/declaredclose.js +9 -2
- package/src/job.js +37 -2
- package/src/judged.js +108 -5
- package/src/kinds.js +12 -0
- package/src/planrun.js +180 -39
- package/src/providers.js +176 -0
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +9 -1
- package/src/run.js +59 -5
- package/src/transport.js +29 -12
- package/types/authorscout.d.ts +27 -2
- package/types/declaredclose.d.ts +6 -1
- package/types/job.d.ts +4 -0
- package/types/judged.d.ts +7 -2
- package/types/kinds.d.ts +9 -0
- package/types/providers.d.ts +104 -0
- package/types/ratelimit.d.ts +59 -0
- package/types/run.d.ts +8 -0
- package/types/transport.d.ts +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,131 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
+
## [0.23.0] — 2026-09-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`openai-api` on the provider menu, behind a provider FACTORY** (PRD item 28,
|
|
13
|
+
F149/F150/F156/F157). `src/providers.js` holds one table — provider name →
|
|
14
|
+
constructor, env key, tier models, per-model request-key gating — and both
|
|
15
|
+
runners go through it instead of naming `AnthropicProvider` directly. An
|
|
16
|
+
unknown provider name THROWS; there is no silent default. `openai-api` takes
|
|
17
|
+
an optional validated job-level `baseUrl` (https, or http on loopback only)
|
|
18
|
+
and admits ONE model, `deepseek-chat`, which earned the slot with a real paid
|
|
19
|
+
green through the shipped runner (run `mtu12vks`: green, $1.64 of $4, 10.3 of
|
|
20
|
+
30 min). DeepSeek silently ignores `max_completion_tokens`, so its table entry
|
|
21
|
+
sets bare-agent's `legacyMaxTokens` — an output cap that does not bind is a
|
|
22
|
+
money hazard. The judge stays PINNED to `anthropic-api` and `JUDGE_MODEL`
|
|
23
|
+
whatever the worker's provider is. `bareloop run` (the bundle runner) is
|
|
24
|
+
`ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would have
|
|
25
|
+
needed, rather than building another provider with the wrong key.
|
|
26
|
+
- **`provider` and `baseUrl` on the `job-start` spine record** (F157),
|
|
27
|
+
report-only. Without them an archive cannot separate anthropic rows from
|
|
28
|
+
`openai-api` rows, and every pooled cost or duration figure would be a
|
|
29
|
+
contaminated aggregate.
|
|
30
|
+
- **`stopReason` on every `worker-round`** (BA-13, report-only, null when
|
|
31
|
+
absent, never invented).
|
|
32
|
+
|
|
33
|
+
- **`runner-drained` — a `beforeExit` backstop for the absent-`job-end` class**
|
|
34
|
+
(PRD item 28's ruling (c); F140). A provider promise that never settles
|
|
35
|
+
(BA-25's class) used to let node drain and exit with NO terminal record at
|
|
36
|
+
all — no escalation, no outcome, no way to tell a silent drain from an
|
|
37
|
+
operator kill (F140: 10-of-235 archived spines with no `job-end`, one real
|
|
38
|
+
silent death). bare-agent 0.42 closed the known drop cases (F141), but the
|
|
39
|
+
class itself is "an instrument fires on absence," not a specific bug, so
|
|
40
|
+
the backstop stays needed. `runJob` (`src/run.js`) now registers ONE
|
|
41
|
+
`process.once('beforeExit', …)` right after `job-start`, tracked via a
|
|
42
|
+
local `ended` flag every `job-end` site sets; if the process still drains
|
|
43
|
+
with no `job-end`, it mints one with outcome `runner-drained`,
|
|
44
|
+
`spendComplete:false` (a drained run's in-flight spend is unknowable — the
|
|
45
|
+
floor already banked is honest, F6), and sets `process.exitCode` non-zero
|
|
46
|
+
(never `process.exit()`). The listener is removed in a `finally` so a
|
|
47
|
+
normal run never leaks it. `runner-drained` is a new terminal outcome,
|
|
48
|
+
documented in `bareloop.context.md`'s outcomes list — deliberately **not**
|
|
49
|
+
added to the resumable/checkpoint set (`CHECKPOINT_OUTCOMES`, `src/reuse.js`):
|
|
50
|
+
an unknown in-flight state is not a known-safe resume point.
|
|
51
|
+
|
|
52
|
+
- **Bounded 429 retry — a FALLBACK, not the primary answer** (PRD item 28's
|
|
53
|
+
parked a/b ruling, hamr verbatim: "do a/b as a fallback, and verify/validate
|
|
54
|
+
+ no regression"; F143). A vendor HTTP 429 used to end the whole run as
|
|
55
|
+
`provider-red`, discarding all completed work, even though the vendor's own
|
|
56
|
+
response states exactly how long to wait. `src/ratelimit.js` (new,
|
|
57
|
+
`transport.js`'s sibling) adds ONE bounded retry that honours that stated
|
|
58
|
+
delay, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS`) — a wait longer than that is
|
|
59
|
+
not honoured at all. Wired into `src/planrun.js`'s provider-retry seam
|
|
60
|
+
(`withTransportRetry` renamed `withProviderRetries`) as a SECOND,
|
|
61
|
+
independent one-shot budget alongside the existing F115 transport retry;
|
|
62
|
+
the wall always wins (no retry past the deadline); each retry emits a
|
|
63
|
+
report-only `rate-limit-retry` spine record and does NOT floor
|
|
64
|
+
`spendComplete` (a 429 is a refusal — nothing was billed — unlike a
|
|
65
|
+
transport throw). **The primary answer stays the primary answer**: picking
|
|
66
|
+
a worker model with adequate rate headroom (F139/F143's measured TPM
|
|
67
|
+
table) — this retry only softens the failure mode when that was not
|
|
68
|
+
followed or headroom was still exceeded.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Bumped `bare-agent` pin from `^0.39.0` to `^0.42.0`. 0.42.0 brings: BA-24 —
|
|
73
|
+
`OpenAIProvider` now sends `max_completion_tokens` by default (GPT-5-safe),
|
|
74
|
+
with a new `legacyMaxTokens` option to send the legacy `max_tokens` key
|
|
75
|
+
instead; BA-25 — a response body cut after headers (peer `'aborted'`,
|
|
76
|
+
stream `'error'`, or a premature `'close'` before `'end'`) now REJECTS
|
|
77
|
+
`generate()` with a retryable `ProviderError` (`context.bound:'transport'`)
|
|
78
|
+
instead of leaving the promise pending forever; a new `loop:truncated`
|
|
79
|
+
stream event plus `stopReason` on the `onLlmResult` metering payload; and
|
|
80
|
+
`options.toolChoice` on `OpenAIProvider`.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **F154** — every provider call bareloop makes now carries a deadline. Seven
|
|
85
|
+
sites had none: the softgreen judge, the calibration gate, the authoring
|
|
86
|
+
scout's three rounds, the declaration/revise boundary, and the bridge picker.
|
|
87
|
+
A silent endpoint HANGS the process (an open socket is an active handle, so
|
|
88
|
+
the drain backstop cannot see it); a deadline is the only instrument that
|
|
89
|
+
fires on the absence of events. Tighten-only.
|
|
90
|
+
- **F155** — the judged floor graded whatever LOCATE reported, so a silently
|
|
91
|
+
omitted function passed on the subset. `decide()` now diffs the artifact's own
|
|
92
|
+
top-level functions against the reported set and reds, naming what was
|
|
93
|
+
missing. The declaration shapes live ONCE (`FN_SHAPES`), from which both the
|
|
94
|
+
locate prompt's prose and the detector are derived.
|
|
95
|
+
- **F153** — a provider's own error sentence now reaches the operator instead of
|
|
96
|
+
a bare status number, redacted through the one secret inventory and capped.
|
|
97
|
+
- **F151** — the bounded 429 retry, added last release, fired on a real vendor
|
|
98
|
+
429 for the first time: it parsed the stated wait from prose, waited it, and
|
|
99
|
+
retried once. Mechanism proven; on a TPM-saturated model the retry is not
|
|
100
|
+
expected to recover, and did not.
|
|
101
|
+
- **F147** — the F59 scout summary round (and the authoring scout's recovery
|
|
102
|
+
round, the same shape in `src/authorscout.js`) re-fed a transcript bare-agent
|
|
103
|
+
had already system-prepended into a new `Loop.run()`, which prepended the
|
|
104
|
+
persona AGAIN: two identical system messages at index 0/1. Real OpenAI
|
|
105
|
+
tolerates that; vLLM-class OpenAI-compatible backends reject it (HTTP 400
|
|
106
|
+
"System message must be at the beginning" — the Qwen death F146 left
|
|
107
|
+
unexplained). Both sites now strip the leading system message before
|
|
108
|
+
continuing, as bare-agent's own `Loop.chat()` does. Proven live: the captured
|
|
109
|
+
failing request goes 400 → 200 with one line removed; deepseek-chat then
|
|
110
|
+
greened a full job (F149/F150).
|
|
111
|
+
- **F141** — `src/transport.js`'s `isTransportFailure` now also recognizes
|
|
112
|
+
bare-agent's own BA-25 transport classification
|
|
113
|
+
(`err.context.bound === 'transport'`), after the existing HTTP-status and
|
|
114
|
+
`retryable:false` guards. Before this fix, the exact class hamr's F115
|
|
115
|
+
one-retry ruling names — a call that dies mid-read, no full response —
|
|
116
|
+
would have ended provider-red with no retry once the 0.42.0 pin landed,
|
|
117
|
+
because the new rejection shape carried none of the signals the classifier
|
|
118
|
+
previously looked for (no `status`, `retryable:true`, no matching error
|
|
119
|
+
code/message). Retry budget unchanged: `TRANSPORT_RETRIES = 1`.
|
|
120
|
+
- **Review door refuses `close-timing-red` instead of defaulting the close timeout**
|
|
121
|
+
(F137, `docs/product/CLOSE-INTEGRITY-BUILD.md` "Ruled 2026-09-07", found by
|
|
122
|
+
orchestrator review of M3, run `mtqwmb9l`). `scripts/run-u.mjs`'s `accept` door path
|
|
123
|
+
resolved the mechanical re-run's close timeout via `resolveCloseTimeoutMs` and, when
|
|
124
|
+
that timing pass itself timed out, silently fell through to `closeTimeoutMs:
|
|
125
|
+
undefined` — the library's 120s default — substituting an unauthorized ceiling for a
|
|
126
|
+
door decision, the exact second silent-default class M3 was built to remove one door
|
|
127
|
+
up. A door whose own timing pass times out now REFUSES outright as a named
|
|
128
|
+
`close-timing-red` door stop (same wording/options as the in-run escalation, new pure
|
|
129
|
+
renderer `doorTimingRedLines` in `scripts/u-readout.mjs`): `answerReviewDoor` is never
|
|
130
|
+
called, nothing is recorded (no door record), nothing is released, nothing is spent,
|
|
131
|
+
and the process exits non-zero.
|
|
132
|
+
|
|
8
133
|
## [0.22.0] — 2026-09-07
|
|
9
134
|
|
|
10
135
|
### 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,19 @@ 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. The JUDGE stays pinned to
|
|
83
|
+
`anthropic-api` and `JUDGE_MODEL` whatever the worker's provider is. The bundle runner
|
|
84
|
+
(`bareloop run`) is `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would
|
|
85
|
+
have needed, rather than constructing another provider with the wrong key.
|
|
73
86
|
- **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
|
|
74
87
|
`bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
|
|
75
88
|
ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
|
|
@@ -85,7 +98,7 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
85
98
|
|
|
86
99
|
```js
|
|
87
100
|
import { runJob, jobSpecHash, makeSpine } from 'bareloop';
|
|
88
|
-
import { AnthropicProvider } from 'bare-agent';
|
|
101
|
+
import { AnthropicProvider } from 'bare-agent/providers';
|
|
89
102
|
|
|
90
103
|
const spec = {
|
|
91
104
|
schema: 'job-v1', job: 'my-maintainer',
|
|
@@ -130,7 +143,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
130
143
|
|---|---|---|
|
|
131
144
|
| `job` | kebab-case slug | |
|
|
132
145
|
| `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` |
|
|
146
|
+
| `provider` | `anthropic-api` \| `openai-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; `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
134
147
|
| `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
148
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
136
149
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
@@ -1008,7 +1021,18 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
|
|
|
1008
1021
|
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
1009
1022
|
plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
|
|
1010
1023
|
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | hitl-pause |
|
|
1011
|
-
hitl-decision-red | step-red:<id
|
|
1024
|
+
hitl-decision-red | step-red:<id> | runner-drained`.
|
|
1025
|
+
|
|
1026
|
+
**`runner-drained` (F140/PRD item 28(c))** is a distinct class from every outcome above: it is
|
|
1027
|
+
NOT `runJob`'s own return value, it is a `beforeExit` backstop's terminal, emitted from outside
|
|
1028
|
+
the normal control flow the instant node drains with no `job-end` at all — a provider promise
|
|
1029
|
+
that never settled (BA-25's class; bare-agent 0.42.0 closed the known drop cases per F141, but
|
|
1030
|
+
"an instrument fires on absence" stays a permanent class, not a fixed bug). It means: exits
|
|
1031
|
+
non-zero, `spendComplete:false` always (a drained run's in-flight spend is unknowable — the
|
|
1032
|
+
floor already banked is honest, an exact-looking total would not be, F6). **It is deliberately
|
|
1033
|
+
NOT in the resumable/checkpoint set** (`CHECKPOINT_OUTCOMES`/`RESUMABLE_HALTS`, `src/reuse.js`)
|
|
1034
|
+
— an unknown in-flight state, unlike a clean `cap-halt`/`wall-halt`/`provider-red`, is not a
|
|
1035
|
+
known-safe resume point.
|
|
1012
1036
|
|
|
1013
1037
|
**`readShim` (default `false`) — the capped read seam, per ARM.** The value names which levers
|
|
1014
1038
|
run — the four arms of the frozen Phase 2 pre-registration:
|
|
@@ -1191,17 +1215,36 @@ round of this attempt being priced repairs nothing about the one before it); and
|
|
|
1191
1215
|
reporting with the rest end of gate"). Every worker Loop (scout, drafter, each step, the fix
|
|
1192
1216
|
worker — the ONE seam in `planrun.js`) gets exactly ONE extra attempt, and only for a
|
|
1193
1217
|
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
|
-
|
|
1218
|
+
`ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause), OR (bare-agent
|
|
1219
|
+
0.42.0, BA-25) a response body cut after headers but before it completed — the provider's own
|
|
1220
|
+
`guardResponseSettles` rejects that as a retryable `ProviderError` tagged
|
|
1221
|
+
`context.bound:'transport'`, which `isTransportFailure` also recognizes (F141). An HTTP response
|
|
1222
|
+
(4xx/5xx generally) is never retried here and rides bare-agent's own unchanged policy. The budget
|
|
1223
|
+
is fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob,
|
|
1224
|
+
tighten-only doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase,
|
|
1225
|
+
attempt, error, recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw
|
|
1226
|
+
before any usage figure came back, so **any** transport retry floors `spendComplete` for the rest
|
|
1227
|
+
of the run even when the retry recovers and the run finishes green — the same one-way flag class
|
|
1228
|
+
as `stalled` and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
|
|
1202
1229
|
so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
|
|
1203
1230
|
field presence and never has to launder a missing `spentUsd` into `$0`.
|
|
1204
1231
|
|
|
1232
|
+
A **rate-limit retry** (PRD item 28's parked a/b ruling, hamr verbatim: "do a/b as a fallback, and
|
|
1233
|
+
verify/validate + no regression"; F143/`src/ratelimit.js`) rides the SAME seam as a second,
|
|
1234
|
+
INDEPENDENT one-shot budget: an HTTP 429 gets ONE retry that honours the vendor's own stated
|
|
1235
|
+
wait, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS` — a longer stated wait is not honoured at all, no
|
|
1236
|
+
retry). This is a FALLBACK, not the primary answer — the primary answer (F139) is picking a
|
|
1237
|
+
worker model with adequate rate headroom; this only softens the failure mode when that was not
|
|
1238
|
+
enough. The wall always wins here too: before sleeping, a retry that would cost more than
|
|
1239
|
+
`clock.remainingMs()` is refused and the error rides straight through, exactly like the transport
|
|
1240
|
+
ladder's own wall carve-out. Each retry emits a report-only `rate-limit-retry` spine record
|
|
1241
|
+
(`{phase, attempt, statedMs, waitedMs, error, recovered}`, no cost field). Unlike the transport
|
|
1242
|
+
retry, a rate-limit retry does **not** floor `spendComplete`: a 429 is a REFUSAL — the vendor
|
|
1243
|
+
rejected the request before processing it, so nothing can have been billed (a stated assumption,
|
|
1244
|
+
not independently measured against a real provider). The two budgets are independent — a
|
|
1245
|
+
transport throw and a 429 may both occur, and both retry, across one call's lifetime, but each at
|
|
1246
|
+
most once.
|
|
1247
|
+
|
|
1205
1248
|
**The plan flow (Layer 2).** `job-start` carries `shape: 'plan'` + the goal; plan steps are
|
|
1206
1249
|
tool-mode by construction. The flow (`runPlan`, also exported for direct callers who own
|
|
1207
1250
|
their own ledger): **close precheck** (`already-green` is a
|
|
@@ -1645,7 +1688,11 @@ that moved (`door-accept-red`) and a missing registry all come back as named red
|
|
|
1645
1688
|
a person) before it is honoured: hamr's ruling that an accept is not a rubber stamp, and the
|
|
1646
1689
|
answer to a tree that can move in the 60 days a door keeps. A red REFUSES the accept with the
|
|
1647
1690
|
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`.
|
|
1691
|
+
green, RELEASES the credit through module 6's `applyDoorDecision`. `scripts/run-u.mjs` resolves
|
|
1692
|
+
the re-run's ceiling itself via `resolveCloseTimeoutMs` before calling this, and refuses outright
|
|
1693
|
+
(F137) as a named `close-timing-red` door stop — `answerReviewDoor` never called, nothing
|
|
1694
|
+
recorded — if that timing pass itself times out, rather than falling through to `runClose`'s
|
|
1695
|
+
library default.
|
|
1649
1696
|
- **`rerun`** carries the person's words back as a directive (`next: 'rerun'`) and re-proves
|
|
1650
1697
|
nothing: the new run's close is what judges. Empty or whitespace text is refused at the same
|
|
1651
1698
|
seam every other door is.
|
|
@@ -2677,7 +2724,7 @@ hash and needs re-approval, exactly like any other semantic edit.
|
|
|
2677
2724
|
| `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
2725
|
| `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
2726
|
| `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) |
|
|
2727
|
+
| `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
2728
|
| `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
2729
|
| `bundle-missing` | `readBundle` | `dir` is not a directory at all |
|
|
2683
2730
|
| `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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
|
},
|
package/src/authorflow.js
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
83
83
|
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES,
|
|
84
84
|
} from './authoring.js';
|
|
85
|
-
import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
|
|
85
|
+
import { buildSeedListing, cleanEntry, SURVEY_CAUSES, AUTHOR_CALL_TIMEOUT_MS } from './authorscout.js';
|
|
86
86
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
|
|
87
87
|
import { redactSecrets } from './validate.js';
|
|
88
88
|
|
|
@@ -1147,7 +1147,12 @@ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens =
|
|
|
1147
1147
|
...t,
|
|
1148
1148
|
execute: async (/** @type {any} */ a) => { const r = await t.execute(a); loop.stop(); return r; },
|
|
1149
1149
|
}));
|
|
1150
|
-
|
|
1150
|
+
// F152/PRD 30.4 — the SAME idle bound `authorscout.js` reuses (this pipeline
|
|
1151
|
+
// has no wall clock of its own): a live endpoint that accepts and never
|
|
1152
|
+
// answers hangs the process forever otherwise. `...opts` still spreads AFTER
|
|
1153
|
+
// it, so a caller that ever needs a tighter number for one call may pass it —
|
|
1154
|
+
// tighten-only, and nothing does today.
|
|
1155
|
+
return loop.run(messages, wired, { cacheMessages: true, maxTokens, timeoutMs: AUTHOR_CALL_TIMEOUT_MS, ...opts });
|
|
1151
1156
|
};
|
|
1152
1157
|
}
|
|
1153
1158
|
|
package/src/authorscout.js
CHANGED
|
@@ -55,11 +55,37 @@ import { TOOL_MENU, WRITE_VERBS, STORE_VERBS } from './job.js';
|
|
|
55
55
|
import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, strategyFor } from './tools.js';
|
|
56
56
|
import { redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
57
57
|
import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
|
|
58
|
+
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
58
59
|
|
|
59
60
|
const require = createRequire(import.meta.url);
|
|
60
61
|
const { Loop, wireGate } = require('bare-agent');
|
|
61
62
|
const { createShellTools } = require('bare-agent/tools');
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* F152/PRD 30.4 — the scout's per-call IDLE bound. Every `loop.run()` in this
|
|
66
|
+
* module carried NO time bound at all: a live endpoint that accepts and never
|
|
67
|
+
* answers hangs the process forever (measured) — an open socket is an active
|
|
68
|
+
* handle, so node never drains and no backstop can fire.
|
|
69
|
+
*
|
|
70
|
+
* UNLIKE the run path (`callBounds()`, src/planrun.js:1518), this pipeline has
|
|
71
|
+
* NO wall clock to derive a bound FROM — the authoring pipeline is genuinely
|
|
72
|
+
* clockless (`src/authorjob.js`/`src/authorflow.js`/`scripts/run-author.mjs`
|
|
73
|
+
* carry a money ceiling, `ceilingUsd`/`capStop`, and never a `maxWallMs`).
|
|
74
|
+
* Rather than inventing a fresh number, this reuses the EXACT ceiling the run
|
|
75
|
+
* path itself falls back to when ITS OWN wall is unbounded
|
|
76
|
+
* (`PROVIDER_TIMEOUT_MS`, `src/clock.js` — BA-18's idle timeout, already an
|
|
77
|
+
* approved "no wall present" default one layer up): an authoring-time call is
|
|
78
|
+
* the identical situation, one call earlier.
|
|
79
|
+
*
|
|
80
|
+
* It bounds SOCKET INACTIVITY only (reset by every byte, per BA-18) — a call
|
|
81
|
+
* that is merely slow keeps streaming and is untouched; only a truly silent
|
|
82
|
+
* endpoint trips it. TIGHTEN-ONLY, like every cap in this file: this is an
|
|
83
|
+
* operator-set ceiling fixed here, not a param a caller or a signed spec may
|
|
84
|
+
* widen (a defaulted cap is a silent second ceiling, and there is no clock
|
|
85
|
+
* whose narrower reading could ever tighten it further).
|
|
86
|
+
*/
|
|
87
|
+
export const AUTHOR_CALL_TIMEOUT_MS = PROVIDER_TIMEOUT_MS;
|
|
88
|
+
|
|
63
89
|
/** src/planrun.js:42 — the survey's hard round bound. */
|
|
64
90
|
export const AUTHOR_SCOUT_ROUNDS = 8;
|
|
65
91
|
/** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
|
|
@@ -348,7 +374,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
348
374
|
*
|
|
349
375
|
* @param {{workdir: string, provider?: any,
|
|
350
376
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
351
|
-
* attempts?: number, ceilingUsd?: number|null,
|
|
377
|
+
* attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
|
|
352
378
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
353
379
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
354
380
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
@@ -365,10 +391,18 @@ export async function runAuthorScout({
|
|
|
365
391
|
ctx = true,
|
|
366
392
|
attempts = SCOUT_ATTEMPTS,
|
|
367
393
|
ceilingUsd = null,
|
|
394
|
+
// F152/PRD 30.4 — TIGHTEN-ONLY, the same clamp shape `attempts` carries just
|
|
395
|
+
// below: an operator (a test included) may buy a SHORTER idle bound and never
|
|
396
|
+
// a longer one. A garbage/absent value floors at the module ceiling
|
|
397
|
+
// (`AUTHOR_CALL_TIMEOUT_MS`), never at "no bound at all".
|
|
398
|
+
callTimeoutMs = AUTHOR_CALL_TIMEOUT_MS,
|
|
368
399
|
onCall = () => {},
|
|
369
400
|
createLoop = defaultLoop,
|
|
370
401
|
createSurveyor = defaultSurveyor,
|
|
371
402
|
}) {
|
|
403
|
+
const callTimeout = Number.isFinite(Number(callTimeoutMs)) && Number(callTimeoutMs) > 0
|
|
404
|
+
? Math.min(Math.trunc(Number(callTimeoutMs)), AUTHOR_CALL_TIMEOUT_MS)
|
|
405
|
+
: AUTHOR_CALL_TIMEOUT_MS;
|
|
372
406
|
const granted = AUTHOR_SCOUT_VERBS;
|
|
373
407
|
const system = SCOUT_SYSTEM + strategyFor([...granted]);
|
|
374
408
|
const surveyor = await createSurveyor({ workdir, granted, ctx });
|
|
@@ -404,6 +438,33 @@ export async function runAuthorScout({
|
|
|
404
438
|
// an ABSENT nobody can act on.
|
|
405
439
|
const allowed = Math.max(1, Math.min(Math.trunc(Number(attempts)) || 1, SCOUT_ATTEMPTS));
|
|
406
440
|
|
|
441
|
+
/**
|
|
442
|
+
* F152/PRD 30.4 — ONE call seam for every `loop.run()` below, so a REJECTED
|
|
443
|
+
* call (a `TimeoutError` off `AUTHOR_CALL_TIMEOUT_MS` included — bare-agent
|
|
444
|
+
* 0.42 rejects every known transport/idle-timeout casualty, F141) lands in the
|
|
445
|
+
* SAME place a call that merely RESOLVED with an error already did: as an
|
|
446
|
+
* `{error}` shape, read by `record()` (cost null, same as a `provider-red`
|
|
447
|
+
* locate call — a rejected call has no knowable cost) and by `classifySurvey`
|
|
448
|
+
* under the EXISTING `call-failed` cause. Without this seam a rejection would
|
|
449
|
+
* escape uncaught to `run-author.mjs`'s top-level crash handler instead of the
|
|
450
|
+
* honest, already-typed terminal this module was built to report through.
|
|
451
|
+
* @param {Promise<any>} p @returns {Promise<any>}
|
|
452
|
+
*/
|
|
453
|
+
const settled = async (p) => {
|
|
454
|
+
try { return await p; } catch (e) {
|
|
455
|
+
// ONLY the call's own deadline lands here. Swallowing every throw would
|
|
456
|
+
// launder a budget HaltError, a programming bug, or any other named
|
|
457
|
+
// terminal into "the survey call failed" — the exact class of dishonesty
|
|
458
|
+
// this repo refuses (an unknown reported as a known). A timeout is the one
|
|
459
|
+
// throw this seam exists for: it IS a call that produced nothing, which is
|
|
460
|
+
// what `call-failed` already means. Everything else re-raises and is
|
|
461
|
+
// relayed after cleanup, as it was before this seam existed.
|
|
462
|
+
const err = /** @type {any} */ (e);
|
|
463
|
+
if (err?.code !== 'ETIMEDOUT' && err?.name !== 'TimeoutError') throw e;
|
|
464
|
+
return { error: String(err?.message ?? err) };
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
407
468
|
try {
|
|
408
469
|
/** @type {Survey|null} */
|
|
409
470
|
let survey = null;
|
|
@@ -455,8 +516,8 @@ export async function runAuthorScout({
|
|
|
455
516
|
};
|
|
456
517
|
loop = createLoop({ system, policy: surveyor.policy, onLlmResult: metered, provider });
|
|
457
518
|
|
|
458
|
-
const r = await loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
459
|
-
{ cacheMessages: true, maxTokens });
|
|
519
|
+
const r = await settled(loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
520
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
460
521
|
verdictAt = record(label, attempt, r);
|
|
461
522
|
blob = redactSecrets(String(r?.text ?? '')).slice(0, blobMax);
|
|
462
523
|
lastMsgs = Array.isArray(r?.msgs) && r.msgs.length ? r.msgs : null;
|
|
@@ -493,8 +554,16 @@ export async function runAuthorScout({
|
|
|
493
554
|
if (recoveryHalt) budgetStop = recoveryHalt;
|
|
494
555
|
if (recoveryWanted && !recoveryHalt) {
|
|
495
556
|
const recovery = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
496
|
-
|
|
497
|
-
|
|
557
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
558
|
+
// 031ffe1): `r.msgs` is a prior `loop.run()`'s returned transcript,
|
|
559
|
+
// which bare-agent already prepended `system` to (Loop.run, loop.js
|
|
560
|
+
// ~482-486). `recovery` below is built with the same `system` and
|
|
561
|
+
// would prepend it again, leaving two identical system messages at
|
|
562
|
+
// index 0/1 — real OpenAI tolerates it, vLLM-class OpenAI-compatible
|
|
563
|
+
// backends 400 on it. Strip the leading one before re-running.
|
|
564
|
+
const rMsgs = r.msgs[0]?.role === 'system' ? r.msgs.slice(1) : r.msgs;
|
|
565
|
+
s2 = await settled(recovery.run([...rMsgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
|
|
566
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
498
567
|
const recoveryAt = record('author-scout-recovery', attempt, s2);
|
|
499
568
|
const t = redactSecrets(String(s2?.text ?? '')).slice(0, blobMax);
|
|
500
569
|
// the verdict follows the blob that WON, never the call that came last
|
|
@@ -518,10 +587,15 @@ export async function runAuthorScout({
|
|
|
518
587
|
// THE RE-ASK. Toolless by construction, over the survey's own
|
|
519
588
|
// conversation, naming only what failed mechanically.
|
|
520
589
|
const reask = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
591
|
+
// 031ffe1): `lastMsgs` is a prior `loop.run()`'s returned transcript
|
|
592
|
+
// (system already prepended by bare-agent); `reask` is built with the
|
|
593
|
+
// same `system` and would prepend it again. Strip the leading one.
|
|
594
|
+
const askMsgs = lastMsgs[0]?.role === 'system' ? lastMsgs.slice(1) : lastMsgs;
|
|
595
|
+
const rr = await settled(reask.run(
|
|
596
|
+
[...askMsgs, { role: 'user', content: scoutReaskTurn(/** @type {Survey} */ (survey).reason ?? '') }], [],
|
|
597
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout },
|
|
598
|
+
));
|
|
525
599
|
verdictAt = record(label, attempt, rr);
|
|
526
600
|
blob = redactSecrets(String(rr?.text ?? '')).slice(0, blobMax);
|
|
527
601
|
if (Array.isArray(rr?.msgs) && rr.msgs.length) lastMsgs = rr.msgs;
|
package/src/calibrate.js
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from './judged.js';
|
|
64
64
|
import { JUDGE_ATTEMPTS } from './kinds.js';
|
|
65
65
|
import { tallyCalls } from './text.js';
|
|
66
|
+
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
66
67
|
|
|
67
68
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
68
69
|
|
|
@@ -330,6 +331,15 @@ async function pipeOnce({ artifactText, card, judgeLoop, attempts, meter, id, ki
|
|
|
330
331
|
card,
|
|
331
332
|
loopFactory: /** @type {any} */ (judgeLoop),
|
|
332
333
|
attempt,
|
|
334
|
+
// F152/PRD 30.4 — this module's own header already names the hazard ("a
|
|
335
|
+
// dead socket") as one of its three CASUALTY readings, but with no bound
|
|
336
|
+
// at all a truly silent judge endpoint never reaches that reading — it
|
|
337
|
+
// hangs the authoring process forever instead (measured). The gate runs
|
|
338
|
+
// at draft/sign time, which has no wall clock to derive a bound from (the
|
|
339
|
+
// authoring pipeline's only ceiling is money, `ceilingUsd`/`capStop`), so
|
|
340
|
+
// this reuses the same idle-timeout default the run path falls back to
|
|
341
|
+
// when ITS OWN wall is unbounded (`PROVIDER_TIMEOUT_MS`, src/clock.js).
|
|
342
|
+
callBounds: { timeoutMs: PROVIDER_TIMEOUT_MS },
|
|
333
343
|
onCost: (c) => meter({
|
|
334
344
|
id, kind, attempt, label: CALIBRATION_LABEL, model: JUDGE_MODEL,
|
|
335
345
|
costUsd: c.costUsd, unpricedRounds: c.unpricedRounds,
|
package/src/cli.js
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
// run, never the unresolved bundleHash. `history.jsonl` records both hashes
|
|
20
20
|
// side by side (POC fact 2) so the pairing can never drift silently.
|
|
21
21
|
|
|
22
|
-
import { createRequire } from 'node:module';
|
|
23
22
|
import { createInterface } from 'node:readline/promises';
|
|
24
23
|
import { hostname } from 'node:os';
|
|
25
24
|
import { execFileSync } from 'node:child_process';
|
|
@@ -34,15 +33,13 @@ import {
|
|
|
34
33
|
exportBundle, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps,
|
|
35
34
|
runJob, makeSpine, loadRegistry, listingRow, jobSpecHash, resolveWorkerModel, JUDGE_MODEL,
|
|
36
35
|
} from './index.js';
|
|
36
|
+
import { resolveProvider, buildRunnerProviders } from './providers.js';
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// spec), so `flagModel` is always undefined and only the spec's own `model`
|
|
44
|
-
// (if signed) or this default is ever in play.
|
|
45
|
-
const DEFAULT_TIER_MODELS = { sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001' };
|
|
38
|
+
// The tier->model tables live in `src/providers.js` now (PRD item 28's
|
|
39
|
+
// factory) — one seam instead of a copy hardcoded in each runner. A
|
|
40
|
+
// `bareloop run` has no `--model` flag (not in the frozen spec), so
|
|
41
|
+
// `flagModel` is always undefined below and only the spec's own `model`
|
|
42
|
+
// (if signed) or the resolved provider's default tier is ever in play.
|
|
46
43
|
|
|
47
44
|
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
48
45
|
const isObj = (v) => typeof v === 'object' && v !== null;
|
|
@@ -58,20 +55,38 @@ const isObj = (v) => typeof v === 'object' && v !== null;
|
|
|
58
55
|
* @param {any} spec the (already `$BARELOOP_BUNDLE`-resolved) bundle spec
|
|
59
56
|
*/
|
|
60
57
|
function buildProviders(apiKey, spec) {
|
|
61
|
-
|
|
58
|
+
// `bareloop run`'s frozen key contract (module header above) is
|
|
59
|
+
// ANTHROPIC_API_KEY only — this `apiKey` is always that key. A bundle
|
|
60
|
+
// whose spec names a provider that reads a DIFFERENT env key therefore
|
|
61
|
+
// has no seam into this flow yet, and the one thing this must never do is
|
|
62
|
+
// construct that provider with the Anthropic key: the run would fail at
|
|
63
|
+
// the first call with a vendor 401, which reads as a credential problem
|
|
64
|
+
// rather than as the unbuilt seam it actually is. So it REFUSES here, at
|
|
65
|
+
// $0, naming the key it would have needed. Widening the contract (a
|
|
66
|
+
// signed per-provider key story) is PRD item 28 part (2)/(3), the
|
|
67
|
+
// arbiter's, not this factory rewire's — the runner that actually fires
|
|
68
|
+
// openai-api jobs today is `scripts/run-u.mjs`, which IS key-aware.
|
|
69
|
+
const providerName = isObj(spec) && typeof spec.provider === 'string' ? spec.provider : 'anthropic-api';
|
|
70
|
+
const entry = resolveProvider(providerName);
|
|
71
|
+
if (entry.envKey !== 'ANTHROPIC_API_KEY') {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`bareloop run: this bundle's spec names provider "${providerName}", which reads `
|
|
74
|
+
+ `${entry.envKey}. The bundle runner's key contract is ANTHROPIC_API_KEY only, so it `
|
|
75
|
+
+ 'refuses rather than calling that provider with the wrong key (PRD item 28 part 2 — '
|
|
76
|
+
+ 'a signed per-provider key story is not built). Run this job through scripts/run-u.mjs.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
62
79
|
const modelResolution = resolveWorkerModel({
|
|
63
80
|
specModel: isObj(spec) && typeof spec.model === 'string' ? spec.model : undefined,
|
|
64
81
|
flagModel: undefined,
|
|
65
|
-
defaultModel:
|
|
82
|
+
defaultModel: entry.tiers.sonnet,
|
|
66
83
|
});
|
|
67
84
|
const MODEL = modelResolution.model;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const judgeProvider = new AnthropicProvider({ apiKey, model: JUDGE_MODEL });
|
|
74
|
-
return { provider, providerFor, judgeProvider };
|
|
85
|
+
const tierModels = modelResolution.source === 'spec' ? { ...entry.tiers, sonnet: MODEL } : entry.tiers;
|
|
86
|
+
const baseUrl = isObj(spec) && typeof spec.baseUrl === 'string' ? spec.baseUrl : undefined;
|
|
87
|
+
return buildRunnerProviders({
|
|
88
|
+
providerName, apiKey, model: MODEL, tierModels, baseUrl, judgeApiKey: apiKey, judgeModel: JUDGE_MODEL,
|
|
89
|
+
});
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
/** @param {string[]} args @returns {{ positional: string[], flags: Record<string, string|true> }} */
|
|
@@ -230,7 +245,13 @@ async function doRun(args, { out, err, cwd, env, now, deps }) {
|
|
|
230
245
|
out(`bundleHash: ${bundle.manifest.bundleHash}`);
|
|
231
246
|
return 0;
|
|
232
247
|
}
|
|
233
|
-
|
|
248
|
+
try {
|
|
249
|
+
({ provider, providerFor, judgeProvider } = buildProviders(apiKey, bundle.spec));
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// a refusal from the key contract above: name it and spend nothing
|
|
252
|
+
err(String(/** @type {any} */ (e)?.message ?? e));
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
// 4. blessing.
|
package/src/declaredclose.js
CHANGED
|
@@ -567,13 +567,14 @@ function translate(r, redact) {
|
|
|
567
567
|
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
568
568
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
569
569
|
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
570
|
-
* onJudgeCost?: ((c: any) => void)|null
|
|
570
|
+
* onJudgeCost?: ((c: any) => void)|null,
|
|
571
|
+
* callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
|
|
571
572
|
* @returns {Promise<any>}
|
|
572
573
|
*/
|
|
573
574
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
574
575
|
const {
|
|
575
576
|
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
576
|
-
judgeLoop = null, onJudgeCost = null,
|
|
577
|
+
judgeLoop = null, onJudgeCost = null, callBounds = null,
|
|
577
578
|
} = opts;
|
|
578
579
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
579
580
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
@@ -611,6 +612,12 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
611
612
|
// until this kind, and a budget must fund the attempt PLUS its close.
|
|
612
613
|
judgeLoop,
|
|
613
614
|
onJudgeCost,
|
|
615
|
+
// F152/PRD 30.4 — the SAME per-call time bounds every worker round already
|
|
616
|
+
// carries (`callBounds()`, src/planrun.js:1518), so the judged stage's own
|
|
617
|
+
// locate call cannot hang the process on a live silent endpoint. `null` is
|
|
618
|
+
// the honest absence (an adopter/test that wires no clock through), never a
|
|
619
|
+
// defaulted number invented here.
|
|
620
|
+
callBounds,
|
|
614
621
|
seedTrees,
|
|
615
622
|
};
|
|
616
623
|
try {
|