bareloop 0.32.0 → 0.33.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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `bareloop run <bundleDir> --repo <path> [--budget N] [--wall MIN] [--approve <bundleHash>]`
3
+ * `bareloop run <bundleDir> --resume <runid> [--repo <path>] [--budget N] [--wall MIN] [--approve <bundleHash>]`
4
+ * @param {string[]} args
5
+ * @param {{
6
+ * out: (s: string) => void, err: (s: string) => void, cwd: string,
7
+ * env: Record<string,string|undefined>, now: () => number, deps: any, keysHome?: string,
8
+ * parseFlags: (a: string[]) => { positional: string[], flags: Record<string, string|true> },
9
+ * printReds: (reds: import('./bundle.js').Red[], err: (s: string) => void) => void,
10
+ * }} ctx
11
+ * @returns {Promise<number>}
12
+ */
13
+ export function bundleMain(args: string[], { out, err, cwd, env, now, deps, keysHome, parseFlags, printReds, }: {
14
+ out: (s: string) => void;
15
+ err: (s: string) => void;
16
+ cwd: string;
17
+ env: Record<string, string | undefined>;
18
+ now: () => number;
19
+ deps: any;
20
+ keysHome?: string;
21
+ parseFlags: (a: string[]) => {
22
+ positional: string[];
23
+ flags: Record<string, string | true>;
24
+ };
25
+ printReds: (reds: import("./bundle.js").Red[], err: (s: string) => void) => void;
26
+ }): Promise<number>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The command that runs `flow`, spelled the way the caller reached this flow: `bareloop <flow>`
3
+ * when `invokedAs` names the CLI, else the source-tree script.
4
+ * @param {Flow} flow @param {string|undefined} invokedAs
5
+ * @returns {string}
6
+ */
7
+ export function commandFor(flow: Flow, invokedAs: string | undefined): string;
8
+ export type Flow = "run-u" | "interview" | "author";
package/types/judged.d.ts CHANGED
@@ -382,9 +382,13 @@ export const JUDGE_RULE_IDS: readonly string[];
382
382
  export const CALIBRATION_SIZE: 10;
383
383
  /** the two verdicts a case may expect — `decide()`'s own closed set */
384
384
  export const CASE_VERDICTS: readonly string[];
385
- export function defaultJudgeLoop({ provider, system }: {
385
+ export function defaultJudgeLoop({ provider, system, rates }: {
386
386
  provider: any;
387
387
  system: string;
388
+ rates?: {
389
+ in: number;
390
+ out: number;
391
+ } | null;
388
392
  }): import("bare-agent").Loop;
389
393
  export type RuleRed = {
390
394
  fn: string;
@@ -29,22 +29,22 @@ export function legTokens(records: any[]): number;
29
29
  /**
30
30
  * Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
31
31
  * and the per-provider figures share.
32
- * @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `now` = the clock for the in-flight test; `resumingSpine` = the spine a resume continues: that leg reserves its spend only, never its unspent cap (the resume's own leg cap is that remainder)
32
+ * @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `now` = the clock for the no-pid in-flight test; `aboveRunid` = read only the runs listed EARLIER in the file than that run (a claim yields to the claims above it, never below)
33
33
  * @returns {Leg[]}
34
34
  */
35
35
  export function readLegs(opts?: {
36
36
  home?: string;
37
37
  now?: () => number;
38
- resumingSpine?: string | null;
38
+ aboveRunid?: string;
39
39
  }): Leg[];
40
40
  /**
41
- * @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `resumingSpine`: see {@link readLegs}
41
+ * @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `aboveRunid`: see {@link readLegs}
42
42
  * @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's)
43
43
  */
44
44
  export function monthSpend(opts?: {
45
45
  home?: string;
46
46
  now?: () => number;
47
- resumingSpine?: string | null;
47
+ aboveRunid?: string;
48
48
  }): {
49
49
  usd: number;
50
50
  atLeast: boolean;
@@ -108,18 +108,18 @@ export function spendSummary(opts?: {
108
108
  */
109
109
  export function monthlyLimitOf(config: Record<string, any>, home?: string): number | null;
110
110
  /**
111
- * Does a run with this $ cap fit in what is left this month (running jobs counted at their full cap, except the spine a resume continues — `resumingSpine` — which counts its real spend)? No limit set (key absent or null) = ok, no check; a limit that is present but not a
111
+ * Does a run with this $ cap fit in what is left this month (a run still held counts at its full cap)? No limit set (key absent or null) = ok, no check; a limit that is present but not a
112
112
  * number above 0 throws `ConfigError`.
113
113
  * Compared in whole cents. Also throws `ConfigError` when config.json is unreadable — a gate
114
114
  * whose own instrument is broken refuses; it never silently runs with no limit.
115
- * @param {{ capUsd: number, home?: string, now?: () => number, resumingSpine?: string|null }} args
115
+ * @param {{ capUsd: number, home?: string, now?: () => number, aboveRunid?: string }} args `aboveRunid`: count only the claims listed above that run (a claim in progress)
116
116
  * @returns {MonthlyRoom}
117
117
  */
118
- export function checkMonthlyRoom({ capUsd, home, now, resumingSpine }: {
118
+ export function checkMonthlyRoom({ capUsd, home, now, aboveRunid }: {
119
119
  capUsd: number;
120
120
  home?: string;
121
121
  now?: () => number;
122
- resumingSpine?: string | null;
122
+ aboveRunid?: string;
123
123
  }): MonthlyRoom;
124
124
  /**
125
125
  * The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
@@ -127,6 +127,40 @@ export function checkMonthlyRoom({ capUsd, home, now, resumingSpine }: {
127
127
  * @returns {string|null} null when the run fits
128
128
  */
129
129
  export function monthlyRefusalText(room: MonthlyRoom): string | null;
130
+ /**
131
+ * Close every claim ABOVE `aboveRunid` (file order) whose process is gone: a pid-bearing run row with no
132
+ * `settled` entry whose pid is not a live bareloop runner gets `{ type: 'settled', by: <closer>, reason:
133
+ * 'process gone', spentUsd: <its spine's floor>, spendComplete: false }` appended — so every claim gets a
134
+ * write-back, attributed. Two runs may both close the same dead claim; readers take the FIRST note.
135
+ * @param {{ home?: string, by: string, aboveRunid?: string }} args `by` = the run doing the closing
136
+ * @returns {number} how many notes were appended
137
+ */
138
+ export function settleDeadClaims({ home, by, aboveRunid }: {
139
+ home?: string;
140
+ by: string;
141
+ aboveRunid?: string;
142
+ }): number;
143
+ /**
144
+ * The CLAIM (hamr 2026-09-30: whoever runs first claims the limit and holds it; no lock file, records
145
+ * only): the run APPENDS its row first (`row` carries its `pid` and `capUsd`), then reads the run list.
146
+ * Only the claims ABOVE its own row count — a live one is held at its cap (a dead one is closed with an
147
+ * attributed note and counts its real spend); rows below yield to it. If `capUsd` fits, it runs; if not
148
+ * it appends `{type:'released', reason:'refused'}` and reports the refusal, nothing spent. No limit set =
149
+ * no claim (`claimed: false`; the caller lists the run as it always did). An unreadable config, or a row
150
+ * that cannot be written, throws `ConfigError` — the gate refuses, never reads "no limit".
151
+ * @param {{ row: import('./runlist.js').RunRow, capUsd: number, home?: string, now?: () => number }} args
152
+ * @returns {{ room: MonthlyRoom, claimed: boolean }}
153
+ */
154
+ export function claimRun({ row, capUsd, home, now }: {
155
+ row: import("./runlist.js").RunRow;
156
+ capUsd: number;
157
+ home?: string;
158
+ now?: () => number;
159
+ }): {
160
+ room: MonthlyRoom;
161
+ claimed: boolean;
162
+ };
163
+ export { isLiveRunner };
130
164
  export type Leg = {
131
165
  /**
132
166
  * when the run was listed
@@ -190,3 +224,4 @@ export type MonthlyRoom = {
190
224
  */
191
225
  atLeast: boolean;
192
226
  };
227
+ import { isLiveRunner } from './runlist.js';
@@ -195,6 +195,10 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
195
195
  * `false` → the drafter has no tools, so a native session would report NO cost — return a
196
196
  * metered claude-json TEXT provider (`--output-format json`, `parse:'claude-json'`) instead,
197
197
  * so its spend is never invisible. The Loop path (`anthropic-api`) never touches this.
198
+ * @param {{in: number, out: number}|null} [opts.rates] the customer's own price for the worker's key row (USD per
199
+ * 1K tokens, `ratesFor`) — handed to the one Loop that drives worker, scout and planner rounds, so they read
200
+ * `rateSource:'caller'`; null = bare-agent's built-in guess. Not applied to a native (CLI) session, which reports its own cost.
201
+ * @param {{in: number, out: number}|null} [opts.judgeRates] the same, for the judge's Loop
198
202
  * @param {string|null} [opts.judgeModel] softgreen — WHICH model that judge provider drives
199
203
  * (PRD item 32.1). Resolved by the CALLER (`resolveJudge`, src/judged.js: the spec's signed
200
204
  * `judge` override, else the job's own worker model), never pinned in this library. It
@@ -416,7 +420,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
416
420
  * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
417
421
  * 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
418
422
  */
419
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
423
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, closeDir, maxStepRounds, layerRoot, readShim, scout, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, priorSpentUsd, reviewDoor, doorRerun, resumable }: {
420
424
  workdir: string;
421
425
  provider: any;
422
426
  nativeProvider?: ((o: {
@@ -425,6 +429,14 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
425
429
  maxTurns: number;
426
430
  hasTools: boolean;
427
431
  }) => any) | undefined;
432
+ rates?: {
433
+ in: number;
434
+ out: number;
435
+ } | null | undefined;
436
+ judgeRates?: {
437
+ in: number;
438
+ out: number;
439
+ } | null | undefined;
428
440
  judgeModel?: string | null | undefined;
429
441
  judgeProvider?: any;
430
442
  emit: (type: string, data?: object) => object;
@@ -23,6 +23,9 @@ export function defaultsFor(envName: string): {
23
23
  * @property {string} name the model id Chat uses ('' = not offered in Chat)
24
24
  * @property {string} provider the `src/providers.js` table entry (the API shape's id)
25
25
  * @property {string} baseUrl the saved Base URL ('' = the shape's default host)
26
+ * @property {unknown} [priceInPerM] the customer's own price, USD per 1M input tokens — carried RAW
27
+ * (present only when the config row names it); {@link ratesFor} is the one place that validates it
28
+ * @property {unknown} [priceOutPerM] the same, for output tokens
26
29
  */
27
30
  /**
28
31
  * The rows: one per name in `filled`, saved entry over defaults.
@@ -122,6 +125,59 @@ export function keyNameFor(provider: string | null | undefined, baseUrl: string
122
125
  * @returns {Record<string,string|undefined>}
123
126
  */
124
127
  export function applyConfiguredKey(env: Record<string, string | undefined>, provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): Record<string, string | undefined>;
128
+ /**
129
+ * The one readout line for a resolved customer price, preview and run tail alike:
130
+ * `yours: in $0.006 / out $1.20 per 1M tokens (DEEPSEEK_API_KEY row)`.
131
+ * @param {{ inPerM: number, outPerM: number, envName: string }} price a {@link ratesFor} result
132
+ * @returns {string}
133
+ */
134
+ export function priceReadout(price: {
135
+ inPerM: number;
136
+ outPerM: number;
137
+ envName: string;
138
+ }): string;
139
+ /**
140
+ * The customer's own price for the row a `(provider, baseUrl[, model])` identity belongs to —
141
+ * the SAME row match {@link keyNameFor} uses (`findRow`), so the price and the key always come
142
+ * from one row. `null` = no price set (the run keeps bare-agent's built-in guess, byte-identical
143
+ * to before). Both fields or neither, each a finite number >= 0, USD per 1M tokens; anything
144
+ * else throws `ConfigError` naming the row and field — a bad price never silently falls back to
145
+ * the guess. `rates` is bare-agent's `Loop({ rates })` shape (USD per 1K); `inPerM`/`outPerM`
146
+ * are what a readout prints.
147
+ * @param {string|null|undefined} provider
148
+ * @param {string|null|undefined} baseUrl
149
+ * @param {readonly KeyRow[]} rows
150
+ * @param {string|null|undefined} [model]
151
+ * @returns {{ rates: { in: number, out: number }, inPerM: number, outPerM: number, envName: string }|null}
152
+ */
153
+ export function ratesFor(provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): {
154
+ rates: {
155
+ in: number;
156
+ out: number;
157
+ };
158
+ inPerM: number;
159
+ outPerM: number;
160
+ envName: string;
161
+ } | null;
162
+ /**
163
+ * The price a judge's rounds are booked at. A price belongs to the MODEL's row: when the judge is
164
+ * the worker's own provider it calls the worker's endpoint, so a row on that endpoint NAMED for the
165
+ * judge's model prices it ({@link ratesFor}); a judge model that is absent or names no row there
166
+ * keeps the worker's price (the worker's row, or none = the guess). A different provider resolves
167
+ * on its own default endpoint, as before. A bad price on the resolved row throws like `ratesFor`.
168
+ * The judge's KEY is chosen elsewhere (`applyConfiguredKey`, by the worker's model), so the two
169
+ * can come from different rows — same provider + endpoint, judge model naming another row.
170
+ * @param {{ provider: string, model?: string|null }} judge
171
+ * @param {string|null|undefined} workerProvider
172
+ * @param {string|null|undefined} workerBaseUrl
173
+ * @param {ReturnType<typeof ratesFor>} workerPrice what the worker's own rounds are booked at
174
+ * @param {readonly KeyRow[]} rows
175
+ * @returns {ReturnType<typeof ratesFor>}
176
+ */
177
+ export function judgeRatesFor(judge: {
178
+ provider: string;
179
+ model?: string | null;
180
+ }, workerProvider: string | null | undefined, workerBaseUrl: string | null | undefined, workerPrice: ReturnType<typeof ratesFor>, rows: readonly KeyRow[]): ReturnType<typeof ratesFor>;
125
181
  /** DeepSeek's OpenAI-shaped endpoint (the same string the panel's Model menu used). */
126
182
  export const DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1";
127
183
  /**
@@ -161,4 +217,13 @@ export type KeyRow = {
161
217
  * the saved Base URL ('' = the shape's default host)
162
218
  */
163
219
  baseUrl: string;
220
+ /**
221
+ * the customer's own price, USD per 1M input tokens — carried RAW
222
+ * (present only when the config row names it); {@link ratesFor} is the one place that validates it
223
+ */
224
+ priceInPerM?: unknown;
225
+ /**
226
+ * the same, for output tokens
227
+ */
228
+ priceOutPerM?: unknown;
164
229
  };
package/types/run.d.ts CHANGED
@@ -34,6 +34,11 @@
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 {{in: number, out: number}|null} [opts.rates] the CUSTOMER's own price for the worker's key row
38
+ * (`ratesFor`, src/providerrows.js — USD per 1K tokens), resolved once by the door and handed to every
39
+ * worker/planner Loop as bare-agent's `rates` (rounds then read `rateSource:'caller'`). Absent = the
40
+ * built-in guess, exactly as before. A Loop-driven provider only: a native session reports its own cost.
41
+ * @param {{in: number, out: number}|null} [opts.judgeRates] the same, for the judge's own key row
37
42
  * @param {string|null} [opts.judgeModel] SOFTGREEN — WHICH model the judge provider drives
38
43
  * (PRD item 32.1), resolved by the CALLER (`resolveJudge`, src/judged.js) and passed
39
44
  * through untouched. Travels with `judgeProvider`; absent, a judged stage stops as a
@@ -204,7 +209,7 @@
204
209
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
205
210
  * unknown in-flight state is not a known-safe resume point.
206
211
  */
207
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, draftSpentUsd, draftSpendComplete, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
212
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, draftSpentUsd, draftSpendComplete, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, resumable }: {
208
213
  approvals: unknown;
209
214
  workdir: string;
210
215
  provider: any;
@@ -216,6 +221,14 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
216
221
  }) => any) | undefined;
217
222
  emit: (type: string, data?: object) => object;
218
223
  providerFor?: ((tier: string) => any) | undefined;
224
+ rates?: {
225
+ in: number;
226
+ out: number;
227
+ } | null | undefined;
228
+ judgeRates?: {
229
+ in: number;
230
+ out: number;
231
+ } | null | undefined;
219
232
  judgeModel?: string | null | undefined;
220
233
  judgeProvider?: any;
221
234
  capRuns?: number | undefined;
@@ -13,7 +13,9 @@ export function runlistHome(override?: string): string;
13
13
  */
14
14
  export function runlistPath(home?: string): string;
15
15
  /**
16
- * @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill' }} RunRow
16
+ * @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill', pid?: number, capUsd?: number }} RunRow
17
+ * `pid`/`capUsd` (optional; older and backfilled rows carry neither): the runner's process id and the
18
+ * leg's $ cap — what the monthly limit holds while that pid is a live bareloop runner (src/monthly.js).
17
19
  */
18
20
  /**
19
21
  * Append one row to the run list, mkdir -p'ing the home directory first.
@@ -42,19 +44,57 @@ export function appendRun(row: RunRow, opts?: {
42
44
  appended: boolean;
43
45
  reason?: "duplicate-runid";
44
46
  };
47
+ /**
48
+ * @typedef {{ runid: string, type: 'settled'|'released', by: string, at: string, reason?: string, spentUsd?: number, spendComplete?: boolean }} RunEvent
49
+ * A claim's write-back, appended (never rewritten) beside the run rows. `settled` = the run's claim on the
50
+ * monthly limit is over (`spentUsd`/`spendComplete` are its final figure, `by` is WHO wrote it: the run itself
51
+ * at its `job-end`, or the next run that found its process gone, `reason: 'process gone'`). `released` = the
52
+ * claim was taken and then given back unspent, its run never started. The month's totals keep reading real
53
+ * spend off the spines; an event only releases the hold and records who wrote it.
54
+ */
55
+ /**
56
+ * Append one {@link RunEvent}. Not locked: one short line, one append. THROWS on IO failure.
57
+ * @param {RunEvent} event
58
+ * @param {{ home?: string }} [opts]
59
+ * @returns {void}
60
+ */
61
+ export function appendRunEvent(event: RunEvent, opts?: {
62
+ home?: string;
63
+ }): void;
45
64
  /**
46
65
  * Tolerant reader — reuses {@link parseJsonl} (never a second hand-rolled
47
66
  * parser). An absent file reads as an empty list, not an error: a fresh
48
- * install has never run `appendRun` yet.
67
+ * install has never run `appendRun` yet. FOLDS the file: `rows` are the RUNS
68
+ * only (an event line is never listed as a run, and a run whose claim was
69
+ * `released` never started, so it is not listed either); `events` are the
70
+ * write-backs, in file order.
49
71
  * @param {{ home?: string }} [opts]
50
- * @returns {{ rows: RunRow[], skipped: number }}
72
+ * @returns {{ rows: RunRow[], skipped: number, events: RunEvent[] }}
51
73
  */
52
74
  export function readRunList(opts?: {
53
75
  home?: string;
54
76
  }): {
55
77
  rows: RunRow[];
56
78
  skipped: number;
79
+ events: RunEvent[];
57
80
  };
81
+ /**
82
+ * Is this pid a live bareloop runner? `process.kill(pid, 0)` says whether the
83
+ * pid exists (EPERM = it exists, just not ours); `/proc/<pid>/cmdline` says whether it is a
84
+ * bareloop runner — a recycled pid that now belongs to some other program is NOT. A runner is
85
+ * a node executable (argv[0] is `node`, `nodejs`, `node22`, ...) with a later argv entry named
86
+ * `bareloop`, `bareloop.mjs`, `run-u.mjs` or `u-watchdog.mjs` (the basename, so the npm bin
87
+ * symlink counts), or a process whose argv[0] is itself one of those names. A non-node program
88
+ * that merely has such a name in its arguments (`nvim /x/bareloop`, `git -C /x/bareloop`) is not
89
+ * a runner. Residual, accepted: a recycled pid that is a node program with such an argument
90
+ * still reads as a runner. The wrong direction to err is "not a runner" on a real live run — it
91
+ * would release that run's cap. Where `/proc` is unreadable (not Linux, or not ours) the
92
+ * existence test alone decides: alive. The ONE spelling — the monthly limit's holds, and
93
+ * `--resume`'s old-pid check share it.
94
+ * @param {number} pid
95
+ * @returns {boolean}
96
+ */
97
+ export function isLiveRunner(pid: number): boolean;
58
98
  /**
59
99
  * `bareloop runs backfill <dir>` — recursively scan `dir` (bounded to
60
100
  * {@link MAX_BACKFILL_DEPTH} levels, never into `node_modules`/`.git`,
@@ -103,6 +143,10 @@ export function backfillRuns(dir: string, opts?: {
103
143
  export function formatRunRow(row: RunRow): string;
104
144
  export const DIED_MTIME_MS: number;
105
145
  export const MAX_BACKFILL_DEPTH: 6;
146
+ /**
147
+ * `pid`/`capUsd` (optional; older and backfilled rows carry neither): the runner's process id and the
148
+ * leg's $ cap — what the monthly limit holds while that pid is a live bareloop runner (src/monthly.js).
149
+ */
106
150
  export type RunRow = {
107
151
  at: string;
108
152
  runid: string;
@@ -110,4 +154,22 @@ export type RunRow = {
110
154
  spine: string;
111
155
  patient: string | null;
112
156
  via: "run-u" | "bundle" | "backfill";
157
+ pid?: number;
158
+ capUsd?: number;
159
+ };
160
+ /**
161
+ * A claim's write-back, appended (never rewritten) beside the run rows. `settled` = the run's claim on the
162
+ * monthly limit is over (`spentUsd`/`spendComplete` are its final figure, `by` is WHO wrote it: the run itself
163
+ * at its `job-end`, or the next run that found its process gone, `reason: 'process gone'`). `released` = the
164
+ * claim was taken and then given back unspent, its run never started. The month's totals keep reading real
165
+ * spend off the spines; an event only releases the hold and records who wrote it.
166
+ */
167
+ export type RunEvent = {
168
+ runid: string;
169
+ type: "settled" | "released";
170
+ by: string;
171
+ at: string;
172
+ reason?: string;
173
+ spentUsd?: number;
174
+ spendComplete?: boolean;
113
175
  };
package/types/source.d.ts CHANGED
@@ -72,15 +72,20 @@ export function looksLikeRepoSource(source: string): boolean;
72
72
  * never hands a repo source's destination to `copyOut` — wiring it into
73
73
  * `writeScope` is M3/M4's job, not this one.
74
74
  * @param {{source: string, into: string, destination?: string,
75
- * fetchTimeoutMs?: number}} args `fetchTimeoutMs` is test-only — production
76
- * callers omit it and get `PROVIDER_TIMEOUT_MS`.
75
+ * fetchTimeoutMs?: number, afterScan?: () => (void|Promise<void>)}} args
76
+ * `fetchTimeoutMs` is test-only — production callers omit it and get
77
+ * `PROVIDER_TIMEOUT_MS`. `afterScan` is a test seam too: called exactly once,
78
+ * after the secret scan finished and before the freeze loop re-reads any
79
+ * file's bytes, so a test can swap a file inside that window
80
+ * deterministically. Production never sets it; absent, behaviour is unchanged.
77
81
  * @returns {Promise<{stop: null, into: string, tree: string, manifestPath: string, manifest: object}|SourceRefusal>}
78
82
  */
79
- export function prepareSource({ source, into, destination, fetchTimeoutMs }: {
83
+ export function prepareSource({ source, into, destination, fetchTimeoutMs, afterScan }: {
80
84
  source: string;
81
85
  into: string;
82
86
  destination?: string;
83
87
  fetchTimeoutMs?: number;
88
+ afterScan?: () => (void | Promise<void>);
84
89
  }): Promise<{
85
90
  stop: null;
86
91
  into: string;
@@ -48,6 +48,11 @@ export type Deps = {
48
48
  * even when `env` is injected; production never sets it.
49
49
  */
50
50
  keysHome?: string | undefined;
51
+ /**
52
+ * the command this run was actually reached through (`src/invoke.js`);
53
+ * `src/cli.js` passes `'bareloop run-u'`, `scripts/run-u.mjs` passes none.
54
+ */
55
+ invokedAs?: string | undefined;
51
56
  out?: ((s: string) => void) | undefined;
52
57
  err?: ((s: string) => void) | undefined;
53
58
  /**
@@ -65,6 +70,45 @@ export type Deps = {
65
70
  */
66
71
  runlistHome?: string | undefined;
67
72
  };
73
+ /**
74
+ * The one-runner door's hand-in (`src/bundlerun.js`, the exported bundle's `bareloop run`):
75
+ * where a bundle run's books live and how it prints its own re-invocation. Set ONLY by
76
+ * that caller through `startRun`/`resumeRun` opts, in-process — `main`'s argv never builds
77
+ * it, so a local job cannot reach it. Absent, every use below is today's expression.
78
+ */
79
+ export type BundleRun = {
80
+ /**
81
+ * the run id (also the `runs/<runid>` directory name)
82
+ */
83
+ runid: string;
84
+ /**
85
+ * `<bundle>/runs/<runid>`: spine.jsonl, gate-audit.jsonl and close/ live here
86
+ */
87
+ runDir: string;
88
+ /**
89
+ * the bare re-invocation prefix printed in resume hints (`bareloop run <bundle> --repo <path>`), no ceiling flags
90
+ */
91
+ invoke: string;
92
+ /**
93
+ * the ceilings the person tightened on THIS leg; printed after `invoke` on every resume line except a money halt's (which never prints the exhausted `--budget`)
94
+ */
95
+ tightened: {
96
+ budgetUsd?: number;
97
+ wallMin?: number;
98
+ };
99
+ /**
100
+ * the bundle's SIGNED spec budgetUsd (the tightened leg ceiling is `spec.budgetUsd`); printed only, never enforced from here
101
+ */
102
+ signedBudgetUsd: number;
103
+ /**
104
+ * the hash printed after `--approve` in those hints (the bundle's, not the run spec's)
105
+ */
106
+ printApprove: string;
107
+ /**
108
+ * creates the run's tree (fresh worktree) and its books; the engine calls it once every $0 refusal has passed, so a refusal leaks nothing
109
+ */
110
+ prepareTree: () => void;
111
+ };
68
112
  export type RunOpts = {
69
113
  /**
70
114
  * the parsed, not-yet-validated job spec — read by
@@ -136,6 +180,10 @@ export type RunOpts = {
136
180
  * how a "go edit the spec" hint should read
137
181
  */
138
182
  specDesc?: string | undefined;
183
+ /**
184
+ * set only by the bundle door (see `BundleRun`)
185
+ */
186
+ bundle?: BundleRun | undefined;
139
187
  deps?: Deps | undefined;
140
188
  };
141
189
  export type RunCtx = {
@@ -159,5 +207,6 @@ export type RunCtx = {
159
207
  door: string | null;
160
208
  decide: string | null;
161
209
  text: string | null;
210
+ bundle?: BundleRun | undefined;
162
211
  deps: Deps;
163
212
  };