bareloop 0.23.0 → 0.25.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.
@@ -31,23 +31,31 @@ export function makeProvider(providerName: string, { apiKey, model, baseUrl }?:
31
31
  * before this factory existed — that decision stays where each runner's own
32
32
  * `--model` flag/spec precedence lives, not duplicated in here).
33
33
  *
34
- * The JUDGE stays pinned to `anthropic-api`/`judgeModel` regardless of
35
- * `providerName` — arbiter territory (PRD item 28: "the judge stays PINNED
36
- * … regardless of the job's worker provider"). A signed `judge:{provider,
37
- * model}` field with its own calibration record is PRD item 28 part (2),
38
- * NOT built here; until it lands, the judge always needs its own
39
- * `judgeApiKey` (today, always `ANTHROPIC_API_KEY`), independent of which
40
- * key the worker used.
34
+ * THE JUDGE IS NO LONGER PINNED TO ANTHROPIC (PRD item 32.2, hamr 2026-09-10:
35
+ * *"what i care about is that judge becomes llm agnostic and not set to one
36
+ * model or provider"*). It is constructed through this same factory, from a
37
+ * `judgeProviderName`/`judgeModel` pair the CALLER resolved (`resolveJudge`,
38
+ * src/judged.js — the spec's signed `judge` override, else the job's own worker
39
+ * provider and model). Item 28's pin, and item 28 part (2)'s deferral of a
40
+ * signed `judge:{provider,model}` field, both close here.
41
+ *
42
+ * `judgeApiKey` stays a SEPARATE argument rather than being derived: the judge
43
+ * may legitimately be a different provider from the worker, and reading its key
44
+ * for it would put a second env-var reader in a module whose whole job is to
45
+ * have exactly one. The caller reads the resolved judge provider's own `envKey`
46
+ * and hands the key in — the same contract the worker key already has here.
41
47
  * @param {object} args
42
48
  * @param {string} args.providerName spec.provider (or the anthropic-api default)
43
49
  * @param {string|undefined} args.apiKey the WORKER's key, read from the provider's own `envKey`
44
50
  * @param {string} args.model the already-resolved worker model id
45
51
  * @param {Readonly<Record<string, string>>} args.tierModels the already-resolved tier -> model map (spec override folded in by the caller)
46
52
  * @param {string|undefined} [args.baseUrl] spec.baseUrl, if any
47
- * @param {string|undefined} args.judgeApiKey the judge's own key (today: `ANTHROPIC_API_KEY`)
48
- * @param {string} args.judgeModel `JUDGE_MODEL`
53
+ * @param {string|undefined} args.judgeApiKey the judge's own key, read by the caller from the RESOLVED judge provider's `envKey`
54
+ * @param {string} args.judgeModel the resolved judge model id
55
+ * @param {string} [args.judgeProviderName] the resolved judge provider name; defaults to `providerName` (the job's own worker provider), which is `resolveJudge`'s own default
56
+ * @param {string|undefined} [args.judgeBaseUrl] the judge provider's endpoint, when it takes one
49
57
  */
50
- export function buildRunnerProviders({ providerName, apiKey, model, tierModels, baseUrl, judgeApiKey, judgeModel, }: {
58
+ export function buildRunnerProviders({ providerName, apiKey, model, tierModels, baseUrl, judgeApiKey, judgeModel, judgeProviderName, judgeBaseUrl, }: {
51
59
  providerName: string;
52
60
  apiKey: string | undefined;
53
61
  model: string;
@@ -55,11 +63,32 @@ export function buildRunnerProviders({ providerName, apiKey, model, tierModels,
55
63
  baseUrl?: string | undefined;
56
64
  judgeApiKey: string | undefined;
57
65
  judgeModel: string;
66
+ judgeProviderName?: string | undefined;
67
+ judgeBaseUrl?: string | undefined;
58
68
  }): {
59
69
  provider: any;
60
70
  providerFor: (tier: string) => any;
61
71
  judgeProvider: any;
62
72
  };
73
+ /**
74
+ * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
75
+ * `null` when the provider has paid for its own end-to-end probe (and for an
76
+ * off-table name — an unknown provider is `resolveProvider`'s named throw to
77
+ * report, never this function's warning to soften).
78
+ *
79
+ * This lives here, next to `PROBE_STATUS`, rather than inline in the runner,
80
+ * for one reason: inline it was UNTESTABLE. `scripts/run-u.mjs` reaches this
81
+ * point only with a real job row (a machine-local patient path) and a real
82
+ * signed spec, so nothing in the suite could reach the branch, and a warning
83
+ * nothing executes drifts exactly the way the green-only prose did (item 31.2).
84
+ *
85
+ * It never REFUSES, and it never returns a shape a caller can mistake for a
86
+ * refusal: hamr admitted `gemini-api` to the menu, and a probe table that
87
+ * quietly withheld it would be a second ruling nobody made.
88
+ * @param {string} providerName
89
+ * @returns {string[]|null} lines to print, in order, or null when there is nothing to say
90
+ */
91
+ export function probeWarningLines(providerName: string): string[] | null;
63
92
  /**
64
93
  * Today's anthropic-api tier table — EXACTLY the `DEFAULT_TIER_MODELS` both
65
94
  * runners hardcoded before this factory existed. Any change to these two
@@ -84,6 +113,35 @@ export const OPENAI_TIER_MODELS: Readonly<{
84
113
  sonnet: "deepseek-chat";
85
114
  haiku: "deepseek-chat";
86
115
  }>;
116
+ /**
117
+ * Gemini (PRD item 31.3, hamr 2026-09-09: "anthropic, openai, gemini drop
118
+ * ollama for now"). Two real tiers, unlike DeepSeek's single model: `flash` is
119
+ * the cheap tier by design, so `haiku` maps to it honestly rather than
120
+ * repeating one id.
121
+ *
122
+ * ADMITTED-PENDING-PROBE. The probe rule stands (PRD item 28 ruling (d),
123
+ * 2026-09-09): no endpoint is in the menu without its own clean paid probe,
124
+ * and `gemini-api` has ZERO runs. `anthropic-api` is the original (~167
125
+ * archived runs) and `openai-api` was probed green through the shipped runner
126
+ * (run mtu12vks, $1.64/$4, F157); gemini owes that fire and has not paid it.
127
+ * `PROBE_STATUS` below is the machine-readable form of that debt — nothing in
128
+ * here silently pretends the provider is proven.
129
+ */
130
+ export const GEMINI_TIER_MODELS: Readonly<{
131
+ sonnet: "gemini-2.5-pro";
132
+ haiku: "gemini-2.5-flash";
133
+ }>;
134
+ /**
135
+ * Which admitted providers have paid for their own end-to-end probe, and which
136
+ * have not. Read by the runner to print a loud marker; never read to REFUSE —
137
+ * hamr admitted gemini to the menu, and a table that quietly withheld it would
138
+ * be a second, invisible ruling. An unprobed provider runs, and says so.
139
+ * @type {Readonly<Record<string, {probed: boolean, evidence: string}>>}
140
+ */
141
+ export const PROBE_STATUS: Readonly<Record<string, {
142
+ probed: boolean;
143
+ evidence: string;
144
+ }>>;
87
145
  export type ProviderTableEntry = {
88
146
  /**
89
147
  * the bare-agent provider class
@@ -98,7 +156,11 @@ export type ProviderTableEntry = {
98
156
  */
99
157
  tiers: Readonly<Record<string, string>>;
100
158
  /**
101
- * per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, baseUrl?}
159
+ * the CONSTRUCTOR OPTION NAME this provider reads its endpoint from — NOT assumed to be `baseUrl` (see below)
160
+ */
161
+ endpointKey: string;
162
+ /**
163
+ * per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, <endpointKey>?}
102
164
  */
103
165
  paramsFor: (model: string | undefined) => Record<string, any>;
104
166
  };
package/types/run.d.ts CHANGED
@@ -34,9 +34,14 @@
34
34
  * metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
35
35
  * @param {(type: string, data?: object) => object} opts.emit spine emitter
36
36
  * @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
37
+ * @param {string|null} [opts.judgeModel] SOFTGREEN — WHICH model the judge provider drives
38
+ * (PRD item 32.1), resolved by the CALLER (`resolveJudge`, src/judged.js) and passed
39
+ * through untouched. Travels with `judgeProvider`; absent, a judged stage stops as a
40
+ * wiring gap rather than grading under an identity nobody named.
37
41
  * @param {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
38
- * locate call through, wired by the operator and PINNED to `JUDGE_MODEL` (src/judged.js:
39
- * the only tier with established injection resistance upstream). Forwarded to the plan
42
+ * locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
43
+ * 32.1: `resolveJudge`/`resolveJobJudge`, src/judged.js — the spec's signed `judge`
44
+ * override, else the job's own worker provider and model), never pinned. Forwarded to the plan
40
45
  * flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
41
46
  * never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
42
47
  * a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
@@ -176,7 +181,7 @@
176
181
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
177
182
  * unknown in-flight state is not a known-safe resume point.
178
183
  */
179
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
184
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
180
185
  approvals: unknown;
181
186
  workdir: string;
182
187
  provider: any;
@@ -188,6 +193,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
188
193
  }) => any) | undefined;
189
194
  emit: (type: string, data?: object) => object;
190
195
  providerFor?: ((tier: string) => any) | undefined;
196
+ judgeModel?: string | null | undefined;
191
197
  judgeProvider?: any;
192
198
  capRuns?: number | undefined;
193
199
  strikeLimit?: number | undefined;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Build the frozen source tree — `<into>/tree/input/…` (the copy), `<into>/tree/output/`
3
+ * (empty, where the run writes), `<into>/source.json` (the manifest, OUTSIDE
4
+ * the tree so no worker can read or edit it). One hidden git commit seeds the
5
+ * tree with a neutralized identity; the seed sha rides in the manifest and is
6
+ * what every close in this run measures against (`seedAtHead`, src/kinds.js).
7
+ *
8
+ * `destination`, when given, is recorded in the manifest (never in a signed
9
+ * job spec — a job spec is a repeatable SHAPE, source/destination a PER-RUN
10
+ * value, see `readSourceManifest`'s own comment) as the run's declared
11
+ * drop-off point. D3 rework (hamr's ruling, 2026-09-12, condensed in
12
+ * `docs/product/ITEM33-BUILD.md`): **destination is a DIRECTORY, never a
13
+ * filename** — it may already exist (need not be empty), and it may sit
14
+ * inside the source itself (the same-dir case: "i can limit your actions to
15
+ * a certain fix in a certain dir and your output will also be there"). A job
16
+ * may produce more than one file; the agent names them, and `copyOut`
17
+ * delivers every non-empty file it finds under `output/`, each under its own
18
+ * dated name. There is therefore no separate `output` field any more — the
19
+ * old single-file `output-invalid` guard existed only to stop a
20
+ * manifest-declared path from escaping the tree, and that vector is closed
21
+ * structurally now: `copyOut` always reads `output/` itself, never a path a
22
+ * manifest could have been hand-edited to name.
23
+ *
24
+ * A `kind: 'repo'` source's `destination` is a DIFFERENT thing entirely
25
+ * (hamr: "destination if for code on a green then that would be the place
26
+ * agent allowed to do changes") — it names the WRITE FENCE inside the copied
27
+ * repo, the signed `writeScope` field's job (`src/job.js:363`), not a
28
+ * filesystem drop-off point this door proves or copies into. It is recorded
29
+ * as declared, unvalidated by `proveDestination`, and `frontDoorFromManifest`
30
+ * never hands a repo source's destination to `copyOut` — wiring it into
31
+ * `writeScope` is M3/M4's job, not this one.
32
+ * @param {{source: string, into: string, destination?: string,
33
+ * fetchTimeoutMs?: number}} args `fetchTimeoutMs` is test-only — production
34
+ * callers omit it and get `PROVIDER_TIMEOUT_MS`.
35
+ * @returns {Promise<{stop: null, into: string, tree: string, manifestPath: string, manifest: object}|SourceRefusal>}
36
+ */
37
+ export function prepareSource({ source, into, destination, fetchTimeoutMs }: {
38
+ source: string;
39
+ into: string;
40
+ destination?: string;
41
+ fetchTimeoutMs?: number;
42
+ }): Promise<{
43
+ stop: null;
44
+ into: string;
45
+ tree: string;
46
+ manifestPath: string;
47
+ manifest: object;
48
+ } | SourceRefusal>;
49
+ /**
50
+ * The delivered name (M2b fix 4, kept under D3): a destination FILE of
51
+ * `/home/me/profile.md` lands as `/home/me/profile-2026-09-12.md`, and a
52
+ * SECOND delivery of the same name the same day as
53
+ * `profile-2026-09-12-2.md`, `-3`, … (the work-branch collision rule, the same
54
+ * spelling `prepareWorkBranch` uses). The date is the delivery's, not the
55
+ * run's: what a person wants to know opening the folder later is when the file
56
+ * arrived. D3: `destination` here is a FULL FILE PATH inside the destination
57
+ * DIRECTORY (`copyOut` builds one per output file it delivers) — this
58
+ * function itself is unchanged from M2b, it is just no longer handed the
59
+ * bare top-level destination directly.
60
+ *
61
+ * Nothing is ever overwritten, which is why the name is computed rather than
62
+ * taken: a plain `profile.md` would eventually collide with the person's own
63
+ * file or a previous run's, and the only honest answers to a collision are
64
+ * "refuse" or "a new name" — hamr picked a new name.
65
+ * @param {string} destination the absolute FILE path as declared
66
+ * @param {Date} now
67
+ * @param {number} n 1 = the plain dated name, 2+ = the same-day suffix
68
+ * @returns {string}
69
+ */
70
+ export function datedDestination(destination: string, now: Date, n: number): string;
71
+ /**
72
+ * The first dated name that is free, or a refusal when every same-day slot up
73
+ * to `MAX_SAME_DAY` is taken. Used by `copyOut` at delivery time (per output
74
+ * file) so a same-day repeat delivery of the same name never disagrees with
75
+ * what "landable" means.
76
+ * @param {string} destination an absolute FILE path
77
+ * @param {Date} [now]
78
+ * @returns {{stop: null, path: string}|SourceRefusal}
79
+ */
80
+ export function pickDelivery(destination: string, now?: Date): {
81
+ stop: null;
82
+ path: string;
83
+ } | SourceRefusal;
84
+ /**
85
+ * Prove a destination DIRECTORY is landable — $0, mechanical, before any
86
+ * token spends (PRD item 33 ruling: "source and destination are proven at
87
+ * job start"). D3 rework (hamr's ruling, 2026-09-12): **destination is a
88
+ * directory, never a filename** — it may already exist, and need not be
89
+ * empty (a job may write into a folder that already holds other files, or
90
+ * even sit inside the source itself, the same-dir case). What this proves is
91
+ * narrower than the M2/M2b version: only that the directory is USABLE
92
+ * (exists as a directory and is writable, or can be created) — it can say
93
+ * nothing about individual delivered FILE names, because those are not known
94
+ * until the run actually produces them (the agent names its own output
95
+ * files); per-file same-day collisions are handled at delivery time, inside
96
+ * `copyOut`, via `pickDelivery`.
97
+ * @param {unknown} destination absolute directory path
98
+ * @param {{into: string}} o `into` — the run's own scratch root (prepared by
99
+ * `prepareSource`, or a run's own `workdir` when there is no source door);
100
+ * the destination may never sit inside it.
101
+ * @returns {Promise<{stop: null}|SourceRefusal>}
102
+ */
103
+ export function proveDestination(destination: unknown, { into }: {
104
+ into: string;
105
+ }): Promise<{
106
+ stop: null;
107
+ } | SourceRefusal>;
108
+ /**
109
+ * Copy the run's own output to the proven destination DIRECTORY — once per
110
+ * file, never overwriting (`COPYFILE_EXCL`, so even a file that appears
111
+ * between `proveDestination` and here refuses rather than clobbers), on a
112
+ * minted green only. This function renders no verdict and changes none: a
113
+ * refusal here is reported beside the green it could not deliver, never
114
+ * folded back into it.
115
+ *
116
+ * D3 rework (hamr's ruling, 2026-09-12): a job may produce MORE THAN ONE
117
+ * file (his example: a flight search producing one sheet for "SFO→LAX
118
+ * red-eye" and another for "SFO under $700") — the agent names them
119
+ * meaningfully, and EVERY non-empty file under `tree/output/` (excluding
120
+ * `output/.gitkeep`) is delivered, each under its own dated name
121
+ * (`datedDestination`/`pickDelivery`, M2b fix 4, unchanged). An empty file is
122
+ * silently skipped (not a delivered result, same judgment M2b made for the
123
+ * single-file case) rather than refusing the whole batch over one empty
124
+ * file; the refusal is reserved for the case where NOTHING is left to
125
+ * deliver at all.
126
+ *
127
+ * `into`, when given, is the run's own SCRATCH ROOT (`prepareSource`'s
128
+ * `into`, of which `tree` is the `tree/` subdirectory) — review finding #3:
129
+ * a destination sitting inside `<into>` but outside `tree` (e.g. dropped
130
+ * straight in the scratch area, never inside the frozen tree) must still
131
+ * refuse `destination-contained`, which a containment check against `tree`
132
+ * alone cannot see. Omitted, it defaults to `tree` (the pre-fix behaviour),
133
+ * for direct callers that only ever had a tree, never an `into`.
134
+ * @param {{tree: string, into?: string, destination: string}} o
135
+ * @returns {Promise<{stop: null, files: {path: string, bytes: number, sha256: string}[]}|SourceRefusal>}
136
+ */
137
+ export function copyOut({ tree, into, destination }: {
138
+ tree: string;
139
+ into?: string;
140
+ destination: string;
141
+ }): Promise<{
142
+ stop: null;
143
+ files: {
144
+ path: string;
145
+ bytes: number;
146
+ sha256: string;
147
+ }[];
148
+ } | SourceRefusal>;
149
+ /**
150
+ * Read a source-door manifest, if one is there. mid-build spec correction
151
+ * (2026-09-11, the main session, on hamr's question about export): a job
152
+ * spec is a SHAPE, signed once — source and destination are a PER-RUN value,
153
+ * the way `bareloop run <bundle> --repo <path>` varies the repo instance
154
+ * without touching the bundle's signature. Baking an absolute destination
155
+ * path into `job.js`/`jobSpecHash` would sign one instance where the spec is
156
+ * meant to describe a repeatable shape, so the run instance lives HERE
157
+ * instead — the manifest `prepareSource` already writes outside the tree —
158
+ * and nothing was added to the job schema.
159
+ *
160
+ * Absence is reported as absence: a patient the JOBS table points at
161
+ * directly, never having gone through `prepareSource`, has no manifest, and
162
+ * that IS "repo jobs untouched" — nothing here invents a destination for a
163
+ * job that never declared one. A manifest that EXISTS but cannot be read or
164
+ * parsed is a named stop, never a silent skip: the run has evidence
165
+ * something is there and wrong, and hiding that is worse than refusing on it.
166
+ * @param {string} into the manifest's own directory — `<into>/source.json`
167
+ * (a run-u-style caller passes `dirname(workdir)`, since `workdir` IS
168
+ * `<into>/tree`)
169
+ * @returns {Promise<{stop: null, present: false, manifest: null}|{stop: null, present: true, manifest: Record<string, any>}|SourceRefusal>}
170
+ */
171
+ export function readSourceManifest(into: string): Promise<{
172
+ stop: null;
173
+ present: false;
174
+ manifest: null;
175
+ } | {
176
+ stop: null;
177
+ present: true;
178
+ manifest: Record<string, any>;
179
+ } | SourceRefusal>;
180
+ /**
181
+ * Reduce a `readSourceManifest` success result to the `{destination}` a
182
+ * runner acts on, or `null` when there is none to act on (no manifest, one
183
+ * that never declared a destination, or — D3 — a `kind: 'repo'` manifest,
184
+ * whose `destination` names the WRITE FENCE inside the copied repo, never a
185
+ * filesystem drop-off point this door proves or copies into: "repo jobs
186
+ * UNCHANGED", `writeScope` already does that job, `src/job.js:363`). Pure
187
+ * extraction of the one piece of `run-u.mjs`'s front-door reading that has
188
+ * any logic in it (the rest is spine emits and control flow) —
189
+ * unit-testable apart from the script, which is otherwise unreachable
190
+ * without a live provider key.
191
+ * @param {{present: boolean, manifest: Record<string, any>|null}} read
192
+ * @returns {{destination: string}|null}
193
+ */
194
+ export function frontDoorFromManifest(read: {
195
+ present: boolean;
196
+ manifest: Record<string, any> | null;
197
+ }): {
198
+ destination: string;
199
+ } | null;
200
+ /**
201
+ * a named, non-throwing stop
202
+ */
203
+ export type SourceRefusal = {
204
+ stop: string;
205
+ code: string;
206
+ };
@@ -226,3 +226,10 @@ export function sweepNestedQuantifiers(root: any, red: (code: string, path: stri
226
226
  */
227
227
  export const CLOSE_SCRIPT_INTERPRETERS: readonly string[];
228
228
  export const SECRET_PATTERNS: RegExp[];
229
+ /** Human-readable name for each entry in `SECRET_PATTERNS`, SAME order, SAME
230
+ * length — for a caller that must name WHICH shape matched (a refusal
231
+ * message, an audit line) without ever printing the matched text itself. Kept
232
+ * beside the inventory it describes rather than hand-typed at each call site,
233
+ * for the same reason `scanSecrets`/`redactSecrets` are: a second, drifting
234
+ * copy is how a name and its pattern come apart. */
235
+ export const SECRET_PATTERN_NAMES: string[];