bareloop 0.30.0 → 0.32.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 (46) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/bareloop.context.md +205 -10
  3. package/bin/bareloop.mjs +0 -1
  4. package/package.json +1 -1
  5. package/src/authorflow.js +22 -42
  6. package/src/authorjob.js +5 -8
  7. package/src/authorrun.js +42 -16
  8. package/src/cli.js +12 -6
  9. package/src/config.js +135 -0
  10. package/src/interviewrun.js +10 -2
  11. package/src/keysfile.js +156 -0
  12. package/src/ledger.js +26 -0
  13. package/src/monthly.js +352 -0
  14. package/src/panel/authorroutes.js +328 -0
  15. package/src/panel/authorsession.js +555 -0
  16. package/src/panel/index.html +1247 -50
  17. package/src/panel/server.js +449 -51
  18. package/src/panel/settingsroutes.js +230 -0
  19. package/src/planrun.js +61 -5
  20. package/src/promptregisters.js +3 -0
  21. package/src/providerrows.js +217 -0
  22. package/src/providers.js +84 -0
  23. package/src/replay.js +80 -2
  24. package/src/replayio.js +14 -3
  25. package/src/run.js +48 -2
  26. package/src/runlist.js +13 -0
  27. package/src/userrun.js +134 -13
  28. package/types/authorflow.d.ts +4 -21
  29. package/types/authorrun.d.ts +5 -0
  30. package/types/cli.d.ts +2 -1
  31. package/types/config.d.ts +48 -0
  32. package/types/interviewrun.d.ts +5 -0
  33. package/types/keysfile.d.ts +90 -0
  34. package/types/ledger.d.ts +14 -0
  35. package/types/monthly.d.ts +192 -0
  36. package/types/panel/authorroutes.d.ts +67 -0
  37. package/types/panel/authorsession.d.ts +53 -0
  38. package/types/panel/server.d.ts +42 -5
  39. package/types/panel/settingsroutes.d.ts +14 -0
  40. package/types/providerrows.d.ts +164 -0
  41. package/types/providers.d.ts +34 -0
  42. package/types/replay.d.ts +31 -0
  43. package/types/replayio.d.ts +12 -1
  44. package/types/run.d.ts +26 -1
  45. package/types/runlist.d.ts +1 -0
  46. package/types/userrun.d.ts +22 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The key value to hand a provider: the `null` marker becomes the placeholder, anything else
3
+ * passes through unchanged.
4
+ * @template {string|undefined} T
5
+ * @param {T} raw
6
+ * @returns {T|string}
7
+ */
8
+ export function usableKey<T extends string | undefined>(raw: T): T | string;
9
+ /**
10
+ * What a key with no saved entry shows. Any name not listed is OpenAI-compatible with a
11
+ * blank URL and a blank Name.
12
+ * @param {string} envName
13
+ * @returns {{ name: string, shape: string, baseUrl: string }}
14
+ */
15
+ export function defaultsFor(envName: string): {
16
+ name: string;
17
+ shape: string;
18
+ baseUrl: string;
19
+ };
20
+ /**
21
+ * @typedef {object} KeyRow
22
+ * @property {string} envName the key variable NAME (read-only label; never a value)
23
+ * @property {string} name the model id Chat uses ('' = not offered in Chat)
24
+ * @property {string} provider the `src/providers.js` table entry (the API shape's id)
25
+ * @property {string} baseUrl the saved Base URL ('' = the shape's default host)
26
+ */
27
+ /**
28
+ * The rows: one per name in `filled`, saved entry over defaults.
29
+ * @param {{ filled: readonly string[], config: Record<string, any> }} src
30
+ * @returns {KeyRow[]}
31
+ */
32
+ export function keyRows({ filled, config }: {
33
+ filled: readonly string[];
34
+ config: Record<string, any>;
35
+ }): KeyRow[];
36
+ /**
37
+ * The rows for a keys home. `undefined` home (a door that skipped the keys file) = no
38
+ * rows, so nothing resolves through the file and the provider's built-in variable stands.
39
+ * @param {string|undefined} home
40
+ * @returns {KeyRow[]}
41
+ */
42
+ export function rowsForHome(home: string | undefined): KeyRow[];
43
+ /**
44
+ * @param {string} provider
45
+ * @returns {string} the shape's default host ('' = the vendor's own)
46
+ */
47
+ export function defaultUrlOf(provider: string): string;
48
+ /**
49
+ * An endpoint in comparable form: blank = the shape's default host; scheme + host lower-cased,
50
+ * trailing slashes dropped.
51
+ * @param {string} provider
52
+ * @param {string|null|undefined} baseUrl
53
+ * @returns {string}
54
+ */
55
+ export function endpointOf(provider: string, baseUrl: string | null | undefined): string;
56
+ /**
57
+ * The URL a row talks to, for display ('' = the vendor's own host, which has no URL to show).
58
+ * @param {KeyRow} row
59
+ * @returns {string}
60
+ */
61
+ export function shownUrl(row: KeyRow): string;
62
+ /**
63
+ * Which row a `(provider, baseUrl[, model])` identity belongs to: the rows on that shape and
64
+ * endpoint; when several share it, the one whose Name is the model, else the first.
65
+ * @param {readonly KeyRow[]} rows
66
+ * @param {{ provider?: string|null, baseUrl?: string|null, model?: string|null }} id
67
+ * @returns {KeyRow|null}
68
+ */
69
+ export function findRow(rows: readonly KeyRow[], { provider, baseUrl, model }: {
70
+ provider?: string | null;
71
+ baseUrl?: string | null;
72
+ model?: string | null;
73
+ }): KeyRow | null;
74
+ /**
75
+ * What Chat's Model menu offers and how a pick resolves: the row whose Name is the model
76
+ * (a blank Name is never offered). `baseUrl` is absent when blank, exactly as a spec omits it.
77
+ * @param {readonly KeyRow[]} rows
78
+ * @param {string} model
79
+ * @returns {{ provider: string, baseUrl?: string, envName: string, name: string }|null}
80
+ */
81
+ export function modelChoiceFor(rows: readonly KeyRow[], model: string): {
82
+ provider: string;
83
+ baseUrl?: string;
84
+ envName: string;
85
+ name: string;
86
+ } | null;
87
+ /**
88
+ * The models Chat offers, in keys-file order.
89
+ * @param {readonly KeyRow[]} rows
90
+ * @returns {{ id: string, envName: string, shape: string }[]}
91
+ */
92
+ export function chatModels(rows: readonly KeyRow[]): {
93
+ id: string;
94
+ envName: string;
95
+ shape: string;
96
+ }[];
97
+ /**
98
+ * The key variable NAME a job on `(provider, baseUrl[, model])` reads: the matching row's
99
+ * variable, else the provider's built-in one (byte-identical to before for anything no row
100
+ * claims).
101
+ * @param {string|null|undefined} provider
102
+ * @param {string|null|undefined} baseUrl
103
+ * @param {readonly KeyRow[]} rows
104
+ * @param {string|null|undefined} [model]
105
+ * @returns {{ name: string, builtIn: string, chosen: boolean }}
106
+ */
107
+ export function keyNameFor(provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): {
108
+ name: string;
109
+ builtIn: string;
110
+ chosen: boolean;
111
+ };
112
+ /**
113
+ * The env a door hands to provider construction, with the matching row's key variable
114
+ * standing in for the provider's built-in one: `env[builtIn] = env[row.envName]` (the `null`
115
+ * no-key marker becomes {@link NO_KEY_PLACEHOLDER}). A copy — never mutates the input.
116
+ * No matching row and no marker = the env unchanged.
117
+ * @param {Record<string,string|undefined>} env
118
+ * @param {string|null|undefined} provider
119
+ * @param {string|null|undefined} baseUrl
120
+ * @param {readonly KeyRow[]} rows
121
+ * @param {string|null|undefined} [model]
122
+ * @returns {Record<string,string|undefined>}
123
+ */
124
+ 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>;
125
+ /** DeepSeek's OpenAI-shaped endpoint (the same string the panel's Model menu used). */
126
+ export const DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1";
127
+ /**
128
+ * The API shapes the dropdown offers. `id` is the `src/providers.js` table name;
129
+ * `defaultUrl` is the host a blank Base URL means ('' = the vendor's own, unshown).
130
+ * @type {readonly {id: string, label: string, defaultUrl: string}[]}
131
+ */
132
+ export const SHAPES: readonly {
133
+ id: string;
134
+ label: string;
135
+ defaultUrl: string;
136
+ }[];
137
+ /** The five empty lines a fresh keys file is created with, in this order. */
138
+ export const PRESET_KEY_NAMES: readonly string[];
139
+ /** A local server's default endpoint: Ollama's OpenAI-compatible host (LM Studio / llama.cpp: change the URL). */
140
+ export const LOCAL_BASE_URL: "http://127.0.0.1:11434/v1";
141
+ /**
142
+ * A key line whose value is the word `null` is SET and means "no real key": the row exists and
143
+ * the provider is handed this harmless stand-in (local servers ignore auth). It is not a
144
+ * secret and is never shown or logged as a key.
145
+ */
146
+ export const NO_KEY_PLACEHOLDER: "no-key-needed";
147
+ export type KeyRow = {
148
+ /**
149
+ * the key variable NAME (read-only label; never a value)
150
+ */
151
+ envName: string;
152
+ /**
153
+ * the model id Chat uses ('' = not offered in Chat)
154
+ */
155
+ name: string;
156
+ /**
157
+ * the `src/providers.js` table entry (the API shape's id)
158
+ */
159
+ provider: string;
160
+ /**
161
+ * the saved Base URL ('' = the shape's default host)
162
+ */
163
+ baseUrl: string;
164
+ };
@@ -98,6 +98,40 @@ export function buildRunnerProviders({ providerName, apiKey, model, tierModels,
98
98
  providerFor: (tier: string) => any;
99
99
  judgeProvider: any;
100
100
  };
101
+ /**
102
+ * $0 reachability check: a GET against `providerName`'s own models-list
103
+ * endpoint, server-side, with the key in a header — NEVER a completion
104
+ * request, so it never spends a token (PANEL-BUILD.md P3 build item 7: "the
105
+ * person must get feedback BEFORE drafting if the chosen model can't
106
+ * work"). Reused, unmodified, by the panel's model-readiness route today
107
+ * (`src/panel/authorroutes.js`) and by the future Settings providers
108
+ * table's own "test" column (PANEL-BUILD.md P4) — ONE function, not two
109
+ * hand-typed copies of the same probe.
110
+ *
111
+ * NEVER returns the key, never logs it — the key only ever appears in a
112
+ * request header sent straight to the provider's own host.
113
+ * @param {object} o
114
+ * @param {string} o.providerName
115
+ * @param {string} o.apiKey
116
+ * @param {string} [o.model] when given, the response is also checked for this model id
117
+ * @param {string} [o.baseUrl]
118
+ * @param {typeof fetch} [o.fetchImpl] TEST SEAM — defaults to the real global `fetch`
119
+ * @param {number} [o.timeoutMs] defaults to 4000 — a flaky endpoint must never hang the caller
120
+ * @returns {Promise<{reachable: boolean, modelListed: boolean|null, status: string, note: string|null}>}
121
+ */
122
+ export function checkProviderReachable({ providerName, apiKey, model, baseUrl, fetchImpl, timeoutMs, }: {
123
+ providerName: string;
124
+ apiKey: string;
125
+ model?: string | undefined;
126
+ baseUrl?: string | undefined;
127
+ fetchImpl?: typeof fetch | undefined;
128
+ timeoutMs?: number | undefined;
129
+ }): Promise<{
130
+ reachable: boolean;
131
+ modelListed: boolean | null;
132
+ status: string;
133
+ note: string | null;
134
+ }>;
101
135
  /**
102
136
  * The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
103
137
  * `null` when the provider has paid for its own end-to-end probe (and for an
package/types/replay.d.ts CHANGED
@@ -89,6 +89,8 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
89
89
  stopReason: string | null;
90
90
  spentUsd: any;
91
91
  spendComplete: boolean;
92
+ draftSpentUsd: any;
93
+ draftSpendComplete: boolean | null;
92
94
  floorReasons: string[];
93
95
  transportRetryCount: number;
94
96
  transportRetriesOutsideWindows: number;
@@ -239,6 +241,35 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, audi
239
241
  memoryCache: any;
240
242
  skipped: number;
241
243
  };
244
+ /**
245
+ * hamr's ruling 2026-09-28 ("one cap covers drafting + run") — the ONE
246
+ * spend display for a run that may carry a drafting share, so every surface
247
+ * that shows a run's money (the CLI's own `spent` line, the panel's Run tab
248
+ * summary/counters, the Runs list row, the Audit header, the Job tab) reads
249
+ * the same string rather than each hand-composing its own. `spendCore` is
250
+ * already-formatted text (`money(...)`, or a `died`/floor line's "at least
251
+ * $X") — this only decides whether to append the `(<draft> drafting)`
252
+ * clause and the `of $<cap>` tail, never re-derives the money itself.
253
+ * Unchanged (`spendCore` alone, or `spendCore of $cap`) when there is no
254
+ * drafting share — a run without one prints exactly what it always did.
255
+ *
256
+ * hamr's ruling 2026-09-28 (2nd addendum) — an INCOMPLETE drafting fold
257
+ * (`draftSpendComplete === false`) is a floor, not a total, and F6's rule
258
+ * ("unpriced is never free, a floor never dresses as exact") applies here
259
+ * exactly as it does to `spendCore` itself: the drafting clause reads `at
260
+ * least $X drafting`, and because a reader summing the two figures would
261
+ * otherwise take the LEADING number as exact too, `spendCore` itself also
262
+ * gains the `at least ` prefix — unless it already carries one (a `died`
263
+ * floor, or an already-prefixed caller), never doubled.
264
+ * @param {string} spendCore already-formatted spend text (e.g. `$3.71` or `at least $2.00`)
265
+ * @param {number|null} draftSpentUsd the drafting share, or null when there isn't one
266
+ * @param {number|null} [budgetUsd] the signed cap, appended as `of $X.XX` when given
267
+ * @param {boolean|null} [draftSpendComplete] whether the drafting fold above was EXACT;
268
+ * `null`/`undefined`/`true` all read as exact (never a new enum value elsewhere) —
269
+ * only an explicit `false` triggers the floor wording.
270
+ * @returns {string}
271
+ */
272
+ export function moneyWithDraft(spendCore: string, draftSpentUsd: number | null, budgetUsd?: number | null, draftSpendComplete?: boolean | null): string;
242
273
  /**
243
274
  * Render a {@link replayRun} summary as one printable page: a header block
244
275
  * (goal/shape/signed/branch/outcome/spent), a timeline (steps or iterations,
@@ -42,7 +42,8 @@ export function resolveSiblings(spinePath: string): {
42
42
  * Read one run's spine (+ its gate-audit sidecar, when present) off disk and
43
43
  * hand both to {@link replayRun}. Read-only, $0, mints no verdict.
44
44
  * @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
45
- * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean}} [opts]
45
+ * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean,
46
+ * auditPathOverride?: string|null}} [opts]
46
47
  * `preParsedSpine`: avoids a second parse of the same file when the caller
47
48
  * ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
48
49
  * `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
@@ -50,6 +51,15 @@ export function resolveSiblings(spinePath: string): {
50
51
  * `summarizeForAllLine` reads none of the fields that sidecar feeds inside
51
52
  * `replayRun`, so reading every run's sidecar in a directory listing was
52
53
  * pure unused I/O, multiplied by every spine found.
54
+ * `auditPathOverride` (build item 7, 2026-09-28): when a caller has ALREADY
55
+ * resolved the real audit path through the one shared resolver
56
+ * (`src/panel/server.js`'s `resolveAuditPathForRow`, which also covers a
57
+ * still-LIVE run's during-run sidecar location — this function's own
58
+ * `resolveSiblings` call only ever finds the FINISHED-run convention), it
59
+ * passes that path here (`null` included) instead of letting this
60
+ * function re-derive a finished-only path on its own. `undefined` (the
61
+ * default) keeps the original resolveSiblings-only behaviour — every
62
+ * existing caller is unaffected.
53
63
  * @returns {ReturnType<typeof replayRun>}
54
64
  */
55
65
  export function replayOne(spinePath: string, opts?: {
@@ -58,6 +68,7 @@ export function replayOne(spinePath: string, opts?: {
58
68
  skipped: number;
59
69
  };
60
70
  skipAudit?: boolean;
71
+ auditPathOverride?: string | null;
61
72
  }): ReturnType<typeof replayRun>;
62
73
  /**
63
74
  * List every spine found directly in `dir` (not recursive — same as the
package/types/run.d.ts CHANGED
@@ -73,6 +73,29 @@
73
73
  * load gate refuses one: a refusal returns the distinct `recipe-stale` outcome with zero
74
74
  * spend rather than silently drafting cold, because starting a paid run on a decision
75
75
  * nobody made is the same class of error as widening a cap to manufacture a green.
76
+ * @param {number} [opts.draftSpentUsd=0] hamr's ruling 2026-09-28 ("one cap covers
77
+ * drafting + run") — money the AUTHORING pipeline (`authorCloseForJob`/
78
+ * `prepareSigning`, run under the SAME signed `budgetUsd` as its ceiling) already
79
+ * spent on THIS job before it was ever signed. The signed `budgetUsd` stays the
80
+ * one number in the spec hash; this shrinks the RUN's own enforced ceiling by
81
+ * what drafting already took, so the two phases never spend more than the one
82
+ * cap together. Deliberately NOT `priorSpentUsd` — that key means "a previous
83
+ * ATTEMPT of this same run died and folded its spend forward" and is read by
84
+ * `readResume`/`replay.js`'s `resumed` flag; a drafting spend is not a resume and
85
+ * must never make an ordinary first run of a job read as one. It is also never
86
+ * folded into `spentUsd` (the run's own ledger, cumulative-so-far and what a
87
+ * resume's `priorSpentUsd` seeds) or into `engagementSpentUsd` — drafting is a
88
+ * DIFFERENT phase's spend, reported beside the run's, never inside it. A garbage
89
+ * value (non-finite, negative) is read as 0, belted exactly like `priorSpentUsd`.
90
+ * @param {boolean} [opts.draftSpendComplete=true] hamr's ruling 2026-09-28 — was the
91
+ * `draftSpentUsd` fold above EXACT? The authoring pipeline's own metered-call list can
92
+ * go unpriced (F6) exactly like a resumed attempt's can, and the unknown does not heal
93
+ * by folding forward. A NEW field beside `draftSpentUsd`, never a new state inside
94
+ * `spendComplete`/`priorSpendComplete` — those answer "is THIS run's own ledger exact,"
95
+ * this answers "was the OTHER phase's ledger exact," and the two must never merge into
96
+ * one flag or a floor drafting spend could read as an exact run. Read only when
97
+ * `draftSpentUsd` itself is emitted; a garbage value is read as `true` (the same belt
98
+ * `priorSpendComplete` gets — an unreadable flag never manufactures a false floor).
76
99
  * @param {number} [opts.priorSpentUsd=0] RESUME (module C) — money a PREVIOUS, killed
77
100
  * attempt of this same try already spent. It seeds the ledger, so the restarted
78
101
  * attempt runs under the REMAINDER of the signed `budgetUsd` and the terminal
@@ -181,7 +204,7 @@
181
204
  * deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
182
205
  * unknown in-flight state is not a known-safe resume point.
183
206
  */
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 }: {
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 }: {
185
208
  approvals: unknown;
186
209
  workdir: string;
187
210
  provider: any;
@@ -201,6 +224,8 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
201
224
  closeTimeoutMs?: number | undefined;
202
225
  closeDir?: string | null | undefined;
203
226
  bridge?: unknown;
227
+ draftSpentUsd?: number | undefined;
228
+ draftSpendComplete?: boolean | undefined;
204
229
  priorSpentUsd?: number | undefined;
205
230
  priorSpendComplete?: boolean | undefined;
206
231
  priorWallMs?: number | undefined;
@@ -101,6 +101,7 @@ export function backfillRuns(dir: string, opts?: {
101
101
  * @returns {string}
102
102
  */
103
103
  export function formatRunRow(row: RunRow): string;
104
+ export const DIED_MTIME_MS: number;
104
105
  export const MAX_BACKFILL_DEPTH: 6;
105
106
  export type RunRow = {
106
107
  at: string;
@@ -43,6 +43,11 @@ export function answerDoor(runId: string, decision: {
43
43
  export function main(argv: string[], deps?: Deps): Promise<number>;
44
44
  export type Deps = {
45
45
  env?: Record<string, string | undefined> | undefined;
46
+ /**
47
+ * P4a — a test seam naming the home whose `.env` keys file is loaded
48
+ * even when `env` is injected; production never sets it.
49
+ */
50
+ keysHome?: string | undefined;
46
51
  out?: ((s: string) => void) | undefined;
47
52
  err?: ((s: string) => void) | undefined;
48
53
  /**
@@ -92,6 +97,21 @@ export type RunOpts = {
92
97
  * the raw `--scout` value (`on`/`off`), or null for the default
93
98
  */
94
99
  scout?: string | null | undefined;
100
+ /**
101
+ * the raw `--draft-spent-usd` value (hamr's
102
+ * ruling 2026-09-28, "one cap covers drafting + run"), or null when the job never
103
+ * drafted through the panel/`bareloop author`. Parsed and validated in `execute`
104
+ * (finite, >= 0, else refused) — never coerced silently, the same param-guard
105
+ * class `--read-shim`/`--scout` already are.
106
+ */
107
+ draftSpentUsd?: string | null | undefined;
108
+ /**
109
+ * the raw `--draft-spend-incomplete` presence-
110
+ * flag (hamr's ruling 2026-09-28, 2nd addendum) — whether the drafting fold above was a
111
+ * FLOOR, never an exact figure. A new field beside `draftSpentUsd`, same param-guard
112
+ * class, no value to misread.
113
+ */
114
+ draftSpendIncomplete?: boolean | undefined;
95
115
  /**
96
116
  * a bridge registry directory
97
117
  */
@@ -129,6 +149,8 @@ export type RunCtx = {
129
149
  model: string | null;
130
150
  readShim: string | null;
131
151
  scout: string | null;
152
+ draftSpentUsd: string | null;
153
+ draftSpendIncomplete: boolean;
132
154
  registry: string | null;
133
155
  workflow: string | null;
134
156
  approve: string | null;