bareloop 0.31.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +4 -1
  3. package/bareloop.context.md +232 -85
  4. package/bin/bareloop.mjs +0 -1
  5. package/package.json +1 -1
  6. package/src/authorflow.js +5 -3
  7. package/src/authorjob.js +3 -1
  8. package/src/authorrun.js +49 -11
  9. package/src/authorscout.js +10 -6
  10. package/src/bundlerun.js +266 -0
  11. package/src/cli.js +35 -357
  12. package/src/config.js +135 -0
  13. package/src/interviewrun.js +13 -4
  14. package/src/invoke.js +22 -0
  15. package/src/judged.js +5 -3
  16. package/src/keysfile.js +156 -0
  17. package/src/ledger.js +26 -0
  18. package/src/monthly.js +422 -0
  19. package/src/panel/authorroutes.js +61 -16
  20. package/src/panel/authorsession.js +46 -28
  21. package/src/panel/index.html +287 -13
  22. package/src/panel/server.js +58 -51
  23. package/src/panel/settingsroutes.js +230 -0
  24. package/src/planrun.js +7 -3
  25. package/src/providerrows.js +301 -0
  26. package/src/providers.js +3 -1
  27. package/src/run.js +7 -2
  28. package/src/runlist.js +92 -7
  29. package/src/source.js +8 -3
  30. package/src/u-watchdog.mjs +296 -0
  31. package/src/userrun.js +230 -54
  32. package/types/authorflow.d.ts +8 -2
  33. package/types/authorjob.d.ts +6 -1
  34. package/types/authorrun.d.ts +5 -0
  35. package/types/authorscout.d.ts +11 -2
  36. package/types/bundlerun.d.ts +26 -0
  37. package/types/cli.d.ts +2 -1
  38. package/types/config.d.ts +48 -0
  39. package/types/interviewrun.d.ts +5 -0
  40. package/types/invoke.d.ts +8 -0
  41. package/types/judged.d.ts +5 -1
  42. package/types/keysfile.d.ts +90 -0
  43. package/types/ledger.d.ts +14 -0
  44. package/types/monthly.d.ts +227 -0
  45. package/types/panel/authorroutes.d.ts +4 -2
  46. package/types/panel/authorsession.d.ts +7 -15
  47. package/types/panel/server.d.ts +10 -3
  48. package/types/panel/settingsroutes.d.ts +14 -0
  49. package/types/planrun.d.ts +13 -1
  50. package/types/providerrows.d.ts +229 -0
  51. package/types/run.d.ts +14 -1
  52. package/types/runlist.d.ts +66 -3
  53. package/types/source.d.ts +8 -3
  54. package/types/userrun.d.ts +54 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @typedef {object} Deps
3
3
  * @property {Record<string,string|undefined>} [env]
4
+ * @property {string} [keysHome] P4a — test seam: load this home's `.env` keys file even when `env` is injected.
4
5
  * @property {NodeJS.ReadStream} [stdin]
5
6
  * @property {NodeJS.WritableStream} [stdout]
6
7
  * @property {NodeJS.WritableStream} [stderr]
@@ -23,6 +24,10 @@
23
24
  export function main(argv: string[], deps?: Deps): Promise<number>;
24
25
  export type Deps = {
25
26
  env?: Record<string, string | undefined> | undefined;
27
+ /**
28
+ * P4a — test seam: load this home's `.env` keys file even when `env` is injected.
29
+ */
30
+ keysHome?: string | undefined;
26
31
  stdin?: NodeJS.ReadStream | undefined;
27
32
  stdout?: NodeJS.WritableStream | undefined;
28
33
  stderr?: NodeJS.WritableStream | undefined;
@@ -93,13 +93,14 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
93
93
  * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
94
  * attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
95
95
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
96
- * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
96
+ * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any, rates?: {in: number, out: number}}) => any,
97
+ * rates?: {in: number, out: number}|null,
97
98
  * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
98
99
  * @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
99
100
  * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
100
101
  * budgetStop: 'cap-halt'|'pricing-red'|null}>}
101
102
  */
102
- export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, createSurveyor, }: {
103
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, rates, createSurveyor, }: {
103
104
  workdir: string;
104
105
  provider?: any;
105
106
  rounds?: number;
@@ -120,7 +121,15 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
120
121
  policy: any;
121
122
  onLlmResult: any;
122
123
  provider: any;
124
+ rates?: {
125
+ in: number;
126
+ out: number;
127
+ };
123
128
  }) => any;
129
+ rates?: {
130
+ in: number;
131
+ out: number;
132
+ } | null;
124
133
  createSurveyor?: (o: {
125
134
  workdir: string;
126
135
  granted: readonly string[];
@@ -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>;
package/types/cli.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
4
4
  * queued stdout on its own (F-something: `process.exit()` can discard it).
5
5
  * @param {string[]} argv
6
- * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string }} deps
6
+ * @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string }} deps
7
7
  * @returns {Promise<number>}
8
8
  */
9
9
  export function main(argv: string[], deps?: {
@@ -18,4 +18,5 @@ export function main(argv: string[], deps?: {
18
18
  now?: () => number;
19
19
  stdin?: any;
20
20
  runlistHome?: string;
21
+ keysHome?: string;
21
22
  }): Promise<number>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @param {string} [home] test seam; production never passes it
3
+ * @returns {string} `<home>/config.json`
4
+ */
5
+ export function configPath(home?: string): string;
6
+ /**
7
+ * @typedef {object} ConfigRead
8
+ * @property {Record<string, any>} config the parsed document ({} when missing/unreadable)
9
+ * @property {boolean} exists
10
+ * @property {string|null} problem plain words when the file is there but unreadable
11
+ */
12
+ /**
13
+ * @param {{ home?: string }} [opts]
14
+ * @returns {ConfigRead}
15
+ */
16
+ export function readConfig(opts?: {
17
+ home?: string;
18
+ }): ConfigRead;
19
+ /**
20
+ * Apply a patch and write. `null` deletes a field. Top-level fields shallow-merge;
21
+ * `keys` merges per key variable name, then per field. Validated before anything is
22
+ * written; nothing is written on a refusal.
23
+ *
24
+ * Patch fields understood: `monthlyLimitUsd` (finite > 0), `anthropicBalanceNote`
25
+ * (finite >= 0), `keys.<ENV NAME>.{name,shape,baseUrl}` (Name = a model id, shape = a provider table name, baseUrl = blank or http(s)). Any other
26
+ * field is passed through untouched (kept), but the whole document is swept for
27
+ * secret-shaped strings first.
28
+ * @param {Record<string, any>} patch
29
+ * @param {{ home?: string }} [opts]
30
+ * @returns {Record<string, any>} the document as written
31
+ */
32
+ export function updateConfig(patch: Record<string, any>, opts?: {
33
+ home?: string;
34
+ }): Record<string, any>;
35
+ /** A plain-words config failure (bad value, secret-shaped string, unreadable file). */
36
+ export class ConfigError extends Error {
37
+ }
38
+ export type ConfigRead = {
39
+ /**
40
+ * the parsed document ({} when missing/unreadable)
41
+ */
42
+ config: Record<string, any>;
43
+ exists: boolean;
44
+ /**
45
+ * plain words when the file is there but unreadable
46
+ */
47
+ problem: string | null;
48
+ };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @typedef {object} Deps
3
3
  * @property {Record<string,string|undefined>} [env]
4
+ * @property {string} [keysHome] P4a — test seam: load this home's `.env` keys file even when `env` is injected.
4
5
  * @property {NodeJS.ReadStream} [stdin]
5
6
  * @property {NodeJS.WritableStream} [stdout]
6
7
  * @property {NodeJS.WritableStream} [stderr]
@@ -25,6 +26,10 @@
25
26
  export function main(argv: string[], deps?: Deps): Promise<number>;
26
27
  export type Deps = {
27
28
  env?: Record<string, string | undefined> | undefined;
29
+ /**
30
+ * P4a — test seam: load this home's `.env` keys file even when `env` is injected.
31
+ */
32
+ keysHome?: string | undefined;
28
33
  stdin?: NodeJS.ReadStream | undefined;
29
34
  stdout?: NodeJS.WritableStream | undefined;
30
35
  stderr?: NodeJS.WritableStream | undefined;
@@ -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;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `~/.config/bareloop` (or the injected override) — same home every other
3
+ * per-person file (runs.jsonl, config.json) lives in.
4
+ * @param {string} [home] test seam; production never passes it
5
+ * @returns {string}
6
+ */
7
+ export function keysHome(home?: string): string;
8
+ /**
9
+ * @param {string} [home]
10
+ * @returns {string} `<home>/.env`
11
+ */
12
+ export function keysFilePath(home?: string): string;
13
+ /**
14
+ * Parse the file's text. Lines that are not `NAME=value` are skipped silently
15
+ * (never echoed — a malformed line may hold a secret).
16
+ * @param {string} text
17
+ * @returns {Record<string,string>}
18
+ */
19
+ export function parseKeysText(text: string): Record<string, string>;
20
+ /**
21
+ * The NAMES in the file that carry a non-empty value, in file order — the Providers rows
22
+ * (an empty preset line is not a row). Names only; a missing/unreadable file = none.
23
+ * @param {string} [home]
24
+ * @returns {string[]}
25
+ */
26
+ export function filledKeyNames(home?: string): string[];
27
+ /**
28
+ * Create the keys file with the five empty preset lines when — and only when — it does not
29
+ * exist (mode 600). An existing file is never touched.
30
+ * @param {readonly string[]} presetNames the names to seed, in order
31
+ * @param {string} [home]
32
+ * @returns {boolean} true if it was created
33
+ */
34
+ export function ensureKeysFile(presetNames: readonly string[], home?: string): boolean;
35
+ /**
36
+ * @typedef {object} KeysLoad
37
+ * @property {boolean} exists the file is there
38
+ * @property {string[]} names the NAMES the file defines (sorted; never values)
39
+ * @property {string|null} warning a plain-words warning (file readable by others, or
40
+ * unreadable), else null — a warning, never a refusal
41
+ * @property {Record<string,string|undefined>} env shell env with the file filling the
42
+ * names the shell does not set. THE ONLY place values leave this module.
43
+ */
44
+ /**
45
+ * Load the keys file over an env. Missing file is fine (no names, env unchanged).
46
+ * @param {{ env?: Record<string,string|undefined>, home?: string }} [opts]
47
+ * @returns {KeysLoad}
48
+ */
49
+ export function loadKeysEnv(opts?: {
50
+ env?: Record<string, string | undefined>;
51
+ home?: string;
52
+ }): KeysLoad;
53
+ /**
54
+ * The door helper the CLI and the panel call: load the file only when the caller is
55
+ * on the REAL environment (no injected env) or names a keys home explicitly — so a
56
+ * test that injects `env` alone never reads the real `~/.config/bareloop/.env`.
57
+ * @param {{ env?: Record<string,string|undefined>, keysHome?: string }} deps
58
+ * @returns {KeysLoad & { env: Record<string,string|undefined>, home: string|undefined }}
59
+ * `home` = the home the file was loaded from (the real `~/.config/bareloop` when none was
60
+ * injected) — or `undefined` when loading was skipped (injected env, no keysHome). The
61
+ * door passes it on so config.json (src/config.js) is read from the SAME place, and never
62
+ * from the real home in a test.
63
+ */
64
+ export function keysForDoor(deps: {
65
+ env?: Record<string, string | undefined>;
66
+ keysHome?: string;
67
+ }): KeysLoad & {
68
+ env: Record<string, string | undefined>;
69
+ home: string | undefined;
70
+ };
71
+ export type KeysLoad = {
72
+ /**
73
+ * the file is there
74
+ */
75
+ exists: boolean;
76
+ /**
77
+ * the NAMES the file defines (sorted; never values)
78
+ */
79
+ names: string[];
80
+ /**
81
+ * a plain-words warning (file readable by others, or
82
+ * unreadable), else null — a warning, never a refusal
83
+ */
84
+ warning: string | null;
85
+ /**
86
+ * shell env with the file filling the
87
+ * names the shell does not set. THE ONLY place values leave this module.
88
+ */
89
+ env: Record<string, string | undefined>;
90
+ };
package/types/ledger.d.ts CHANGED
@@ -81,6 +81,20 @@ export function spendProvenance(events: any[]): {
81
81
  unpriced: ProvenanceBucket;
82
82
  unknown: ProvenanceBucket;
83
83
  };
84
+ /**
85
+ * The priced-rounds spend floor + first-record→last-record wall floor,
86
+ * derived straight off the spine's raw records — the same derivation a
87
+ * died run's "at least …" figures use, factored out so build item 6
88
+ * (2026-09-28) can hand a genuinely-still-running spine the identical
89
+ * floor instead of "unknown" on the Run tab, without a second copy of this
90
+ * math.
91
+ * @param {any[]} records raw parsed spine records
92
+ * @returns {{spendFloorUsd: number|null, wallFloorMs: number|null}}
93
+ */
94
+ export function floorsFromRecords(records: any[]): {
95
+ spendFloorUsd: number | null;
96
+ wallFloorMs: number | null;
97
+ };
84
98
  /** severity order, worst-first (the fold's display order; frequency ranks within) */
85
99
  export const LEDGER_CLASSES: readonly string[];
86
100
  /**
@@ -0,0 +1,227 @@
1
+ /**
2
+ * One run's own spend, off its spine records: this LEG's figure (never the chain fold),
3
+ * plus the drafting spend the run was signed with (counted once — on the first leg only).
4
+ * @param {any[]} records
5
+ * @returns {{ usd: number, complete: boolean }}
6
+ */
7
+ export function legSpend(records: any[]): {
8
+ usd: number;
9
+ complete: boolean;
10
+ };
11
+ /**
12
+ * One run's wall time off its spine: job-start -> job-end when the run ended (exact), else the
13
+ * first -> last record floor (the SAME floor a died / still-running run's "at least" wall uses,
14
+ * `floorsFromRecords`). `ms` null = unknown — never 0.
15
+ * @param {any[]} records
16
+ * @returns {{ ms: number|null, complete: boolean }}
17
+ */
18
+ export function legWall(records: any[]): {
19
+ ms: number | null;
20
+ complete: boolean;
21
+ };
22
+ /**
23
+ * Tokens a spine's rounds processed (input + output + cache read + cache write — all four,
24
+ * so a cache-heavy run is not under-reported), off the real `usage` field names.
25
+ * @param {any[]} records
26
+ * @returns {number}
27
+ */
28
+ export function legTokens(records: any[]): number;
29
+ /**
30
+ * Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
31
+ * and the per-provider figures share.
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
+ * @returns {Leg[]}
34
+ */
35
+ export function readLegs(opts?: {
36
+ home?: string;
37
+ now?: () => number;
38
+ aboveRunid?: string;
39
+ }): Leg[];
40
+ /**
41
+ * @param {{ home?: string, now?: () => number, aboveRunid?: string }} [opts] `aboveRunid`: see {@link readLegs}
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
+ */
44
+ export function monthSpend(opts?: {
45
+ home?: string;
46
+ now?: () => number;
47
+ aboveRunid?: string;
48
+ }): {
49
+ usd: number;
50
+ atLeast: boolean;
51
+ runs: number;
52
+ reservedUsd: number;
53
+ };
54
+ /**
55
+ * The Money tab's figures: all-time and this-month spend, and per-provider breakdown
56
+ * (rows from `rowIdFor`; a run on no known row lands under its own provider name, never
57
+ * pooled into another row). Any leg with unknown spend makes the figure it belongs to
58
+ * an "at least".
59
+ * @param {{ home?: string, now?: () => number, rows?: readonly import('./providerrows.js').KeyRow[] }} [opts] `rows` = the Providers rows: when given, `tokensByRow` sums each row's tokens
60
+ * @returns {{ tokensByRow: Record<string, number>, total: {usd: number, atLeast: boolean, tokens: number}, month: {usd: number, atLeast: boolean, tokens: number},
61
+ * byProvider: Record<string, {label: string, monthUsd: number, monthAtLeast: boolean, totalUsd: number, totalAtLeast: boolean, tokens: number, vouchedRounds: number, otherRounds: number, monthWallMs: number|null, monthWallAtLeast: boolean, totalWallMs: number|null, totalWallAtLeast: boolean}> }}
62
+ */
63
+ export function spendSummary(opts?: {
64
+ home?: string;
65
+ now?: () => number;
66
+ rows?: readonly import("./providerrows.js").KeyRow[];
67
+ }): {
68
+ tokensByRow: Record<string, number>;
69
+ total: {
70
+ usd: number;
71
+ atLeast: boolean;
72
+ tokens: number;
73
+ };
74
+ month: {
75
+ usd: number;
76
+ atLeast: boolean;
77
+ tokens: number;
78
+ };
79
+ byProvider: Record<string, {
80
+ label: string;
81
+ monthUsd: number;
82
+ monthAtLeast: boolean;
83
+ totalUsd: number;
84
+ totalAtLeast: boolean;
85
+ tokens: number;
86
+ vouchedRounds: number;
87
+ otherRounds: number;
88
+ monthWallMs: number | null;
89
+ monthWallAtLeast: boolean;
90
+ totalWallMs: number | null;
91
+ totalWallAtLeast: boolean;
92
+ }>;
93
+ };
94
+ /**
95
+ * @typedef {object} MonthlyRoom
96
+ * @property {boolean} ok
97
+ * @property {number|null} leftUsd what is left this month (null = no limit set)
98
+ * @property {number|null} limitUsd the person's monthly limit (null = none)
99
+ * @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
100
+ */
101
+ /**
102
+ * The person's monthly limit off a parsed config.json: key absent (or null) = no limit (null);
103
+ * a number above 0 = the limit; ANY other value present is a broken setting and throws
104
+ * `ConfigError` — a limit the file names but this code cannot read never reads as "no limit".
105
+ * @param {Record<string, any>} config
106
+ * @param {string} [home]
107
+ * @returns {number|null}
108
+ */
109
+ export function monthlyLimitOf(config: Record<string, any>, home?: string): number | null;
110
+ /**
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
+ * number above 0 throws `ConfigError`.
113
+ * Compared in whole cents. Also throws `ConfigError` when config.json is unreadable — a gate
114
+ * whose own instrument is broken refuses; it never silently runs with no limit.
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
+ * @returns {MonthlyRoom}
117
+ */
118
+ export function checkMonthlyRoom({ capUsd, home, now, aboveRunid }: {
119
+ capUsd: number;
120
+ home?: string;
121
+ now?: () => number;
122
+ aboveRunid?: string;
123
+ }): MonthlyRoom;
124
+ /**
125
+ * The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
126
+ * @param {MonthlyRoom} room
127
+ * @returns {string|null} null when the run fits
128
+ */
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 };
164
+ export type Leg = {
165
+ /**
166
+ * when the run was listed
167
+ */
168
+ at: Date;
169
+ /**
170
+ * job-start's provider (null = an older spine that carried none)
171
+ */
172
+ provider: string | null;
173
+ baseUrl: string | null;
174
+ /**
175
+ * this leg's spend (a floor when `!complete`)
176
+ */
177
+ usd: number;
178
+ /**
179
+ * every figure behind `usd` is known
180
+ */
181
+ complete: boolean;
182
+ tokens: number;
183
+ /**
184
+ * job-start's model (older spines carry a model but no provider)
185
+ */
186
+ model: string | null;
187
+ /**
188
+ * this leg's wall time (null = unknown; a floor when `!wallComplete`)
189
+ */
190
+ wallMs: number | null;
191
+ /**
192
+ * the wall is the exact job-start -> job-end span
193
+ */
194
+ wallComplete: boolean;
195
+ /**
196
+ * rounds priced by a rate somebody vouched for (`provider`/`caller`)
197
+ */
198
+ vouchedRounds: number;
199
+ /**
200
+ * every other round: a built-in guess, unpriced, or no provenance on record
201
+ */
202
+ otherRounds: number;
203
+ /**
204
+ * what the refusal check counts: `usd`, or the leg's cap when the run is in flight
205
+ */
206
+ reservedUsd: number;
207
+ /**
208
+ * the spine was missing/unreadable or the listed date was bad (spend unknown)
209
+ */
210
+ unreadable: boolean;
211
+ };
212
+ export type MonthlyRoom = {
213
+ ok: boolean;
214
+ /**
215
+ * what is left this month (null = no limit set)
216
+ */
217
+ leftUsd: number | null;
218
+ /**
219
+ * the person's monthly limit (null = none)
220
+ */
221
+ limitUsd: number | null;
222
+ /**
223
+ * the month's spend is a floor, so `leftUsd` is a ceiling
224
+ */
225
+ atLeast: boolean;
226
+ };
227
+ import { isLiveRunner } from './runlist.js';
@@ -25,7 +25,7 @@ export function checkHumanGuard(req: import("node:http").IncomingMessage, { toke
25
25
  * deliberately has none of).
26
26
  * @param {{ port: number, token: string, env?: Record<string,string|undefined>,
27
27
  * sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
28
- * jobsDir?: string, fetchImpl?: typeof fetch }} opts
28
+ * jobsDir?: string, fetchImpl?: typeof fetch, home?: string }} opts
29
29
  */
30
30
  export function createAuthorRoutes(opts: {
31
31
  port: number;
@@ -36,6 +36,7 @@ export function createAuthorRoutes(opts: {
36
36
  bareloopBin?: string;
37
37
  jobsDir?: string;
38
38
  fetchImpl?: typeof fetch;
39
+ home?: string;
39
40
  }): {
40
41
  handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
41
42
  sessions: Map<string, any>;
@@ -48,7 +49,7 @@ export function createAuthorRoutes(opts: {
48
49
  * it to sign"), and this is where it is checked, not trusted.
49
50
  * @param {ReturnType<typeof createSession>} session
50
51
  * @param {string} claimedHash
51
- * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string}} o
52
+ * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string, home?: string}} o
52
53
  * @returns {{ok: boolean, error?: string, job?: string}}
53
54
  */
54
55
  export function signRun(session: ReturnType<typeof createSession>, claimedHash: string, o: {
@@ -56,6 +57,7 @@ export function signRun(session: ReturnType<typeof createSession>, claimedHash:
56
57
  spawnFn: typeof realSpawn;
57
58
  bareloopBin: string;
58
59
  sessionsRoot?: string;
60
+ home?: string;
59
61
  }): {
60
62
  ok: boolean;
61
63
  error?: string;
@@ -16,9 +16,13 @@ export function jobNameTaken(name: string, opts?: {
16
16
  * creates a session or spends anything — never inside the async pipeline,
17
17
  * so a bad card refuses synchronously, in the same HTTP response.
18
18
  * @param {any} card
19
+ * @param {{jobsDir?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card
19
20
  * @returns {{ok: true}|{ok: false, error: string}}
20
21
  */
21
- export function validateJobCard(card: any): {
22
+ export function validateJobCard(card: any, opts?: {
23
+ jobsDir?: string;
24
+ rows?: readonly import("../providerrows.js").KeyRow[];
25
+ }): {
22
26
  ok: true;
23
27
  } | {
24
28
  ok: false;
@@ -30,7 +34,7 @@ export function validateJobCard(card: any): {
30
34
  * is the ONE thing a person answers next; `answer()`/`send()`/`revise()`/
31
35
  * `signPrepare()` are the only ways in.
32
36
  * @param {any} card the validated job card (see {@link validateJobCard})
33
- * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, timeoutMs?: number,
37
+ * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
34
38
  * scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
35
39
  * prepareSigningFn?: Function}} [deps] the last five are TEST SEAMS ONLY — see the note
36
40
  * just above where each is read, below.
@@ -39,6 +43,7 @@ export function validateJobCard(card: any): {
39
43
  export function createSession(card: any, deps?: {
40
44
  env?: Record<string, string | undefined>;
41
45
  sessionsRoot?: string;
46
+ home?: string;
42
47
  timeoutMs?: number;
43
48
  scout?: any;
44
49
  generate?: Function;
@@ -46,16 +51,3 @@ export function createSession(card: any, deps?: {
46
51
  authorFn?: Function;
47
52
  prepareSigningFn?: Function;
48
53
  }): any;
49
- /**
50
- * The job card's "Model" select — a fixed, small, source-grounded mapping
51
- * from a display id to the `{provider, baseUrl?}` pair `src/interviewrun.js`
52
- * itself documents (that script's own `--provider openai-api --base-url
53
- * https://api.deepseek.com/v1` usage line for DeepSeek): the panel builds no
54
- * provider table of its own, this is a UI convenience over the SAME two
55
- * provider identities the CLI already resolves through `resolveProvider`.
56
- * @type {Readonly<Record<string, {provider: string, baseUrl?: string}>>}
57
- */
58
- export const MODEL_OPTIONS: Readonly<Record<string, {
59
- provider: string;
60
- baseUrl?: string;
61
- }>>;
@@ -211,13 +211,14 @@ export function getRunJob(runid: string, opts?: {
211
211
  * only `createPanelServer` below always supplies one.
212
212
  * @param {import('node:http').IncomingMessage} req
213
213
  * @param {import('node:http').ServerResponse} res
214
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes> }} opts
214
+ * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
215
215
  */
216
216
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
217
217
  home?: string;
218
218
  port: number;
219
219
  token?: string;
220
220
  authorRoutes?: ReturnType<typeof createAuthorRoutes>;
221
+ settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
221
222
  }): void;
222
223
  /**
223
224
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -259,13 +260,17 @@ export function createPanelServer(opts?: {
259
260
  * Never picks a different port on collision (see file header): prints a
260
261
  * loud, named error to stderr and returns 1.
261
262
  * @param {string[]} argv
262
- * @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string }} ctx
263
+ * @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string, env?: Record<string,string|undefined> }} ctx
264
+ * `env` is the RAW shell env (never the start-time merge with the keys file) — the routes
265
+ * re-merge `~/.config/bareloop/.env` onto it per request so Reload keys is real;
266
+ * undefined = the process env.
263
267
  * @returns {Promise<number>}
264
268
  */
265
269
  export function panelMain(argv: string[], ctx: {
266
270
  out: (s: string) => void;
267
271
  err: (s: string) => void;
268
272
  runlistHome?: string;
273
+ env?: Record<string, string | undefined>;
269
274
  }): Promise<number>;
270
275
  /**
271
276
  * A runid, as it appears in a URL path segment — never a raw filesystem
@@ -286,10 +291,12 @@ export const DEFAULT_PORT: 4700;
286
291
  * 2026-09-25).
287
292
  */
288
293
  export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
289
- export const DIED_MTIME_MS: number;
294
+ export { DIED_MTIME_MS };
290
295
  /** The default/max page size for {@link getRunRounds} — tighten-only,
291
296
  * named so a future change is a deliberate, visible edit (item 5,
292
297
  * 2026-09-25). */
293
298
  export const ROUNDS_PAGE_DEFAULT: 50;
294
299
  export const ROUNDS_PAGE_MAX: 200;
295
300
  import { createAuthorRoutes } from './authorroutes.js';
301
+ import { createSettingsRoutes } from './settingsroutes.js';
302
+ import { DIED_MTIME_MS } from '../runlist.js';