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,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,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,192 @@
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, 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)
33
+ * @returns {Leg[]}
34
+ */
35
+ export function readLegs(opts?: {
36
+ home?: string;
37
+ now?: () => number;
38
+ resumingSpine?: string | null;
39
+ }): Leg[];
40
+ /**
41
+ * @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `resumingSpine`: 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
+ resumingSpine?: string | null;
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 (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
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, resumingSpine?: string|null }} args
116
+ * @returns {MonthlyRoom}
117
+ */
118
+ export function checkMonthlyRoom({ capUsd, home, now, resumingSpine }: {
119
+ capUsd: number;
120
+ home?: string;
121
+ now?: () => number;
122
+ resumingSpine?: string | null;
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
+ export type Leg = {
131
+ /**
132
+ * when the run was listed
133
+ */
134
+ at: Date;
135
+ /**
136
+ * job-start's provider (null = an older spine that carried none)
137
+ */
138
+ provider: string | null;
139
+ baseUrl: string | null;
140
+ /**
141
+ * this leg's spend (a floor when `!complete`)
142
+ */
143
+ usd: number;
144
+ /**
145
+ * every figure behind `usd` is known
146
+ */
147
+ complete: boolean;
148
+ tokens: number;
149
+ /**
150
+ * job-start's model (older spines carry a model but no provider)
151
+ */
152
+ model: string | null;
153
+ /**
154
+ * this leg's wall time (null = unknown; a floor when `!wallComplete`)
155
+ */
156
+ wallMs: number | null;
157
+ /**
158
+ * the wall is the exact job-start -> job-end span
159
+ */
160
+ wallComplete: boolean;
161
+ /**
162
+ * rounds priced by a rate somebody vouched for (`provider`/`caller`)
163
+ */
164
+ vouchedRounds: number;
165
+ /**
166
+ * every other round: a built-in guess, unpriced, or no provenance on record
167
+ */
168
+ otherRounds: number;
169
+ /**
170
+ * what the refusal check counts: `usd`, or the leg's cap when the run is in flight
171
+ */
172
+ reservedUsd: number;
173
+ /**
174
+ * the spine was missing/unreadable or the listed date was bad (spend unknown)
175
+ */
176
+ unreadable: boolean;
177
+ };
178
+ export type MonthlyRoom = {
179
+ ok: boolean;
180
+ /**
181
+ * what is left this month (null = no limit set)
182
+ */
183
+ leftUsd: number | null;
184
+ /**
185
+ * the person's monthly limit (null = none)
186
+ */
187
+ limitUsd: number | null;
188
+ /**
189
+ * the month's spend is a floor, so `leftUsd` is a ceiling
190
+ */
191
+ atLeast: boolean;
192
+ };
@@ -0,0 +1,67 @@
1
+ /** @returns {string} a fresh per-process token — never persisted, never logged */
2
+ export function mintToken(): string;
3
+ /**
4
+ * `true` when a request may reach ANY route in this file — the token header
5
+ * plus an Origin/Host that names this exact bind address. `req.headers`
6
+ * lower-cases header names (node's own `http` behaviour), so this reads
7
+ * lower-case names throughout.
8
+ * @param {import('node:http').IncomingMessage} req
9
+ * @param {{token: string, port: number}} o
10
+ * @returns {{ok: true}|{ok: false, reason: string}}
11
+ */
12
+ export function checkHumanGuard(req: import("node:http").IncomingMessage, { token, port }: {
13
+ token: string;
14
+ port: number;
15
+ }): {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ reason: string;
20
+ };
21
+ /**
22
+ * Builds this file's own request handler, holding its session store in a
23
+ * closure (one per `createPanelServer` call, exactly like `handleRequest`'s
24
+ * own module holds none — this file owns the state `src/panel/server.js`
25
+ * deliberately has none of).
26
+ * @param {{ port: number, token: string, env?: Record<string,string|undefined>,
27
+ * sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
28
+ * jobsDir?: string, fetchImpl?: typeof fetch, home?: string }} opts
29
+ */
30
+ export function createAuthorRoutes(opts: {
31
+ port: number;
32
+ token: string;
33
+ env?: Record<string, string | undefined>;
34
+ sessionsRoot?: string;
35
+ spawnFn?: typeof realSpawn;
36
+ bareloopBin?: string;
37
+ jobsDir?: string;
38
+ fetchImpl?: typeof fetch;
39
+ home?: string;
40
+ }): {
41
+ handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
42
+ sessions: Map<string, any>;
43
+ };
44
+ /**
45
+ * THE ONLY function in this file (or anywhere in the panel) that spawns a
46
+ * run. Refuses unless the session is `prepared` AND the hash the request
47
+ * carried matches the session's OWN `signing.json` `specHash` exactly — the
48
+ * click carries the hash (per the mockup's own "the person never has to copy
49
+ * it to sign"), and this is where it is checked, not trusted.
50
+ * @param {ReturnType<typeof createSession>} session
51
+ * @param {string} claimedHash
52
+ * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string, home?: string}} o
53
+ * @returns {{ok: boolean, error?: string, job?: string}}
54
+ */
55
+ export function signRun(session: ReturnType<typeof createSession>, claimedHash: string, o: {
56
+ env: any;
57
+ spawnFn: typeof realSpawn;
58
+ bareloopBin: string;
59
+ sessionsRoot?: string;
60
+ home?: string;
61
+ }): {
62
+ ok: boolean;
63
+ error?: string;
64
+ job?: string;
65
+ };
66
+ import { spawn as realSpawn } from 'node:child_process';
67
+ import { createSession } from './authorsession.js';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `true` when `jobs/<name>.json` already exists in the repo's own jobs/ dir
3
+ * (resolved from THIS module's own on-disk location, never `process.cwd()`,
4
+ * the same rule `src/panel/server.js`'s `jobsDir()` follows) — P3 is new-jobs-
5
+ * only (Q4=A), so a name collision refuses at $0 rather than silently
6
+ * overwriting or authoring a second spec under one name.
7
+ * @param {string} name
8
+ * @param {{jobsDir?: string}} [opts] test seam
9
+ * @returns {boolean}
10
+ */
11
+ export function jobNameTaken(name: string, opts?: {
12
+ jobsDir?: string;
13
+ }): boolean;
14
+ /**
15
+ * `$0` validation of the job card fields the server must check BEFORE it
16
+ * creates a session or spends anything — never inside the async pipeline,
17
+ * so a bad card refuses synchronously, in the same HTTP response.
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
20
+ * @returns {{ok: true}|{ok: false, error: string}}
21
+ */
22
+ export function validateJobCard(card: any, opts?: {
23
+ jobsDir?: string;
24
+ rows?: readonly import("../providerrows.js").KeyRow[];
25
+ }): {
26
+ ok: true;
27
+ } | {
28
+ ok: false;
29
+ error: string;
30
+ };
31
+ /**
32
+ * ONE session's engine — created already RUNNING (the caller awaits nothing;
33
+ * the pipeline drives itself, reporting through `state`). `state.pendingAsk`
34
+ * is the ONE thing a person answers next; `answer()`/`send()`/`revise()`/
35
+ * `signPrepare()` are the only ways in.
36
+ * @param {any} card the validated job card (see {@link validateJobCard})
37
+ * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
38
+ * scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
39
+ * prepareSigningFn?: Function}} [deps] the last five are TEST SEAMS ONLY — see the note
40
+ * just above where each is read, below.
41
+ * @returns {any} the session object
42
+ */
43
+ export function createSession(card: any, deps?: {
44
+ env?: Record<string, string | undefined>;
45
+ sessionsRoot?: string;
46
+ home?: string;
47
+ timeoutMs?: number;
48
+ scout?: any;
49
+ generate?: Function;
50
+ confirmGenerate?: Function;
51
+ authorFn?: Function;
52
+ prepareSigningFn?: Function;
53
+ }): any;
@@ -200,13 +200,25 @@ export function getRunJob(runid: string, opts?: {
200
200
  * separately from {@link createPanelServer} so tests can drive it without a
201
201
  * real listening socket where that is simpler (most path-safety/405 tests
202
202
  * still go through a real socket, per the build spec).
203
+ *
204
+ * PANEL-BUILD.md P3: this file's own GET/HEAD-only rule stands for every
205
+ * route it owns; `/api/author/*` is the ONE family of routes that may be
206
+ * POSTed to, and it is handled entirely by {@link createAuthorRoutes}
207
+ * (`src/panel/authorroutes.js`) — a separate module so this file's own
208
+ * "read-only, by construction" header comment stays true of everything else
209
+ * in it. `opts.authorRoutes` is absent on every P1/P2 caller (read-only
210
+ * tests, and the CLI's own `panelMain` when no author routes are wired) —
211
+ * only `createPanelServer` below always supplies one.
203
212
  * @param {import('node:http').IncomingMessage} req
204
213
  * @param {import('node:http').ServerResponse} res
205
- * @param {{ home?: string, port: number }} opts
214
+ * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
206
215
  */
207
216
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
208
217
  home?: string;
209
218
  port: number;
219
+ token?: string;
220
+ authorRoutes?: ReturnType<typeof createAuthorRoutes>;
221
+ settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
210
222
  }): void;
211
223
  /**
212
224
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -214,15 +226,33 @@ export function handleRequest(req: import("node:http").IncomingMessage, res: imp
214
226
  * header). Resolves once actually listening; rejects on a bind error
215
227
  * (including `EADDRINUSE`) with a `.port` field on the error for the
216
228
  * caller's message.
217
- * @param {{ port?: number, home?: string }} [opts]
218
- * @returns {Promise<{ server: import('node:http').Server, port: number, close: () => Promise<void> }>}
229
+ *
230
+ * PANEL-BUILD.md P3: a fresh {@link mintToken} token and one {@link
231
+ * createAuthorRoutes} instance are built here, ONCE per server start, and
232
+ * held for the server's lifetime — `opts.env`/`opts.sessionsRoot`/
233
+ * `opts.spawnFn`/`opts.bareloopBin` are test seams the author routes take
234
+ * (a real caller passes none of them and {@link createAuthorRoutes} defaults
235
+ * each one to the real environment/spawn/binary, this file itself touching
236
+ * none of them, and the real
237
+ * `~/.config/bareloop/panel-sessions`, the real `child_process.spawn`, and
238
+ * this package's own `bin/bareloop.mjs`).
239
+ * @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
240
+ * sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
241
+ * fetchImpl?: typeof fetch }} [opts]
242
+ * @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
219
243
  */
220
244
  export function createPanelServer(opts?: {
221
245
  port?: number;
222
246
  home?: string;
247
+ env?: Record<string, string | undefined>;
248
+ sessionsRoot?: string;
249
+ spawnFn?: (...a: any[]) => any;
250
+ bareloopBin?: string;
251
+ fetchImpl?: typeof fetch;
223
252
  }): Promise<{
224
253
  server: import("node:http").Server;
225
254
  port: number;
255
+ token: string;
226
256
  close: () => Promise<void>;
227
257
  }>;
228
258
  /**
@@ -230,13 +260,17 @@ export function createPanelServer(opts?: {
230
260
  * Never picks a different port on collision (see file header): prints a
231
261
  * loud, named error to stderr and returns 1.
232
262
  * @param {string[]} argv
233
- * @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.
234
267
  * @returns {Promise<number>}
235
268
  */
236
269
  export function panelMain(argv: string[], ctx: {
237
270
  out: (s: string) => void;
238
271
  err: (s: string) => void;
239
272
  runlistHome?: string;
273
+ env?: Record<string, string | undefined>;
240
274
  }): Promise<number>;
241
275
  /**
242
276
  * A runid, as it appears in a URL path segment — never a raw filesystem
@@ -257,9 +291,12 @@ export const DEFAULT_PORT: 4700;
257
291
  * 2026-09-25).
258
292
  */
259
293
  export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
260
- export const DIED_MTIME_MS: number;
294
+ export { DIED_MTIME_MS };
261
295
  /** The default/max page size for {@link getRunRounds} — tighten-only,
262
296
  * named so a future change is a deliberate, visible edit (item 5,
263
297
  * 2026-09-25). */
264
298
  export const ROUNDS_PAGE_DEFAULT: 50;
265
299
  export const ROUNDS_PAGE_MAX: 200;
300
+ import { createAuthorRoutes } from './authorroutes.js';
301
+ import { createSettingsRoutes } from './settingsroutes.js';
302
+ import { DIED_MTIME_MS } from '../runlist.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @param {{ port: number, token: string, home?: string, env?: Record<string,string|undefined>,
3
+ * fetchImpl?: typeof fetch, now?: () => number }} opts
4
+ */
5
+ export function createSettingsRoutes(opts: {
6
+ port: number;
7
+ token: string;
8
+ home?: string;
9
+ env?: Record<string, string | undefined>;
10
+ fetchImpl?: typeof fetch;
11
+ now?: () => number;
12
+ }): {
13
+ handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
14
+ };