bareloop 0.33.0 → 0.35.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.
- package/CHANGELOG.md +47 -0
- package/bareloop.context.md +263 -27
- package/package.json +1 -1
- package/src/bundlerun.js +13 -4
- package/src/draftspend.js +82 -0
- package/src/exits.js +10 -1
- package/src/interviewrun.js +10 -9
- package/src/job.js +39 -2
- package/src/judged.js +25 -5
- package/src/kinds.js +2 -0
- package/src/ledger.js +104 -9
- package/src/legs.js +171 -0
- package/src/monthly.js +133 -87
- package/src/panel/authorroutes.js +95 -15
- package/src/panel/authorsession.js +408 -113
- package/src/panel/importroutes.js +359 -0
- package/src/panel/importrun.js +203 -0
- package/src/panel/index.html +1071 -184
- package/src/panel/runroutes.js +219 -0
- package/src/panel/server.js +855 -81
- package/src/panel/status.js +51 -0
- package/src/planrun.js +63 -31
- package/src/providerrows.js +21 -2
- package/src/providers.js +4 -4
- package/src/ralph.js +3 -0
- package/src/replay.js +139 -19
- package/src/replayio.js +11 -4
- package/src/reuse.js +20 -3
- package/src/run.js +6 -3
- package/src/runlist.js +90 -7
- package/src/text.js +20 -0
- package/src/u-readout.js +9 -1
- package/src/u-watchdog.mjs +4 -2
- package/src/userrun.js +166 -65
- package/types/draftspend.d.ts +53 -0
- package/types/job.d.ts +25 -0
- package/types/judged.d.ts +5 -2
- package/types/ledger.d.ts +44 -0
- package/types/legs.d.ts +103 -0
- package/types/monthly.d.ts +39 -22
- package/types/panel/authorroutes.d.ts +38 -1
- package/types/panel/authorsession.d.ts +109 -2
- package/types/panel/importroutes.d.ts +129 -0
- package/types/panel/importrun.d.ts +77 -0
- package/types/panel/runroutes.d.ts +32 -0
- package/types/panel/server.d.ts +255 -2
- package/types/panel/status.d.ts +51 -0
- package/types/planrun.d.ts +13 -21
- package/types/providerrows.d.ts +14 -1
- package/types/providers.d.ts +1 -2
- package/types/replay.d.ts +25 -1
- package/types/replayio.d.ts +2 -2
- package/types/reuse.d.ts +5 -0
- package/types/run.d.ts +6 -2
- package/types/runlist.d.ts +66 -2
- package/types/text.d.ts +19 -0
package/types/legs.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split one spine's parsed records into its legs. Pure.
|
|
3
|
+
* @param {any[]} events one spine's parsed records, in file order
|
|
4
|
+
* @returns {Leg[]} always at least one leg for a non-empty list; `[]` for an empty one
|
|
5
|
+
*/
|
|
6
|
+
export function legsOf(events: any[]): Leg[];
|
|
7
|
+
/**
|
|
8
|
+
* The legs' WORKING time, gap excluded (hamr 2026-10-02, 2A: only working time charges the wall). Each leg
|
|
9
|
+
* counts first record → last record; the quiet time between one leg's end and the next leg's marker is
|
|
10
|
+
* nobody's. `null` = unknown (no leg carries two parseable stamps) — never 0. A leg whose stamps are
|
|
11
|
+
* unreadable adds nothing, so the figure is a FLOOR, `complete: false`.
|
|
12
|
+
* @param {Leg[]} legs
|
|
13
|
+
* @returns {{ ms: number|null, complete: boolean }}
|
|
14
|
+
*/
|
|
15
|
+
export function legsWallMs(legs: Leg[]): {
|
|
16
|
+
ms: number | null;
|
|
17
|
+
complete: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parse a spine's TEXT, tolerant exactly where the writer leaves damage. A kill mid-append leaves a torn
|
|
21
|
+
* line; a resume never edits it (the spine is append-only forever) — it writes one `\n` and starts a fresh
|
|
22
|
+
* line, so the torn bytes end up alone on their own line, immediately BEFORE a `leg-resume` marker. That
|
|
23
|
+
* line, and the file's own last two lines (the older tail rule: a kill mid-append of the file's final
|
|
24
|
+
* record), are tolerated and named. An unparseable line anywhere else is `corrupt`: a reconstruction from
|
|
25
|
+
* it would invent a history.
|
|
26
|
+
* @param {string} raw
|
|
27
|
+
* @returns {{ records: any[], tolerated: {line: number, why: 'tail'|'before-leg-resume'}[], corrupt: {line: number, message: string}|null }}
|
|
28
|
+
* `line` is 1-based. Only the FIRST corrupt line is reported.
|
|
29
|
+
*/
|
|
30
|
+
export function parseSpineText(raw: string): {
|
|
31
|
+
records: any[];
|
|
32
|
+
tolerated: {
|
|
33
|
+
line: number;
|
|
34
|
+
why: "tail" | "before-leg-resume";
|
|
35
|
+
}[];
|
|
36
|
+
corrupt: {
|
|
37
|
+
line: number;
|
|
38
|
+
message: string;
|
|
39
|
+
} | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Where leg `leg`'s watchdog kill note lives. Leg 1 keeps the name every run has always had
|
|
43
|
+
* (`<spine>.watchdog.json`); a resumed leg's note sits beside it (`<spine>.leg<N>.watchdog.json`) so one
|
|
44
|
+
* leg never overwrites another's evidence of how it died.
|
|
45
|
+
* @param {string} spineFile
|
|
46
|
+
* @param {number} leg 1-based
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function watchdogNotePath(spineFile: string, leg: number): string;
|
|
50
|
+
/**
|
|
51
|
+
* P5 item 5 (Stop) — where a run's STOP REQUEST lives: a file beside its spine. The panel's stop route writes it;
|
|
52
|
+
* the engine reads it at the between-steps seam (src/planrun.js) and consumes it. One spelling, here, so the route
|
|
53
|
+
* and the engine can never name two files. A request belongs to ONE leg: the engine deletes it when it honours it,
|
|
54
|
+
* when the leg ends, and again at the start of the next leg (src/userrun.js).
|
|
55
|
+
* @param {string} spineFile
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function stopFilePath(spineFile: string): string;
|
|
59
|
+
/** The marker record's `type`. */
|
|
60
|
+
export const LEG_RESUME: "leg-resume";
|
|
61
|
+
export type Leg = {
|
|
62
|
+
/**
|
|
63
|
+
* 1-based position in file order (the marker's own `leg` field is never trusted over the order)
|
|
64
|
+
*/
|
|
65
|
+
leg: number;
|
|
66
|
+
/**
|
|
67
|
+
* the leg's first record (the `leg-resume` marker for leg 2+; the file's first record for leg 1)
|
|
68
|
+
*/
|
|
69
|
+
start: any | null;
|
|
70
|
+
/**
|
|
71
|
+
* the leg's last record
|
|
72
|
+
*/
|
|
73
|
+
end: any | null;
|
|
74
|
+
/**
|
|
75
|
+
* every record of the leg, in file order, marker included
|
|
76
|
+
*/
|
|
77
|
+
records: any[];
|
|
78
|
+
/**
|
|
79
|
+
* the leg's own `job-start` (null: it ended before runJob wrote one)
|
|
80
|
+
*/
|
|
81
|
+
jobStart: any | null;
|
|
82
|
+
/**
|
|
83
|
+
* the leg's own LAST `job-end` (null: it died, or it is still running)
|
|
84
|
+
*/
|
|
85
|
+
jobEnd: any | null;
|
|
86
|
+
/**
|
|
87
|
+
* the leg's terminal: its `job-end` outcome, else a `run-end` outcome (a $0 refusal), else null
|
|
88
|
+
*/
|
|
89
|
+
outcome: string | null;
|
|
90
|
+
/**
|
|
91
|
+
* leg 1: null. Leg 2+: how the PREVIOUS leg ended, as the marker declares it
|
|
92
|
+
* (else derived from that leg's own terminal), `'died'` when it recorded none
|
|
93
|
+
*/
|
|
94
|
+
after: string | null;
|
|
95
|
+
/**
|
|
96
|
+
* `Date.parse` of the start record's `ts`
|
|
97
|
+
*/
|
|
98
|
+
startMs: number | null;
|
|
99
|
+
/**
|
|
100
|
+
* `Date.parse` of the end record's `ts`
|
|
101
|
+
*/
|
|
102
|
+
endMs: number | null;
|
|
103
|
+
};
|
package/types/monthly.d.ts
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
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
1
|
/**
|
|
12
2
|
* One run's wall time off its spine: job-start -> job-end when the run ended (exact), else the
|
|
13
3
|
* first -> last record floor (the SAME floor a died / still-running run's "at least" wall uses,
|
|
14
4
|
* `floorsFromRecords`). `ms` null = unknown — never 0.
|
|
15
|
-
* @param {any[]} records
|
|
5
|
+
* @param {any[]} records one spine's records (one leg or several)
|
|
16
6
|
* @returns {{ ms: number|null, complete: boolean }}
|
|
17
7
|
*/
|
|
18
8
|
export function legWall(records: any[]): {
|
|
@@ -29,20 +19,23 @@ export function legTokens(records: any[]): number;
|
|
|
29
19
|
/**
|
|
30
20
|
* Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
|
|
31
21
|
* 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)
|
|
22
|
+
* @param {{ home?: string, sessionsRoot?: string, now?: () => number, aboveRunid?: string, thisMonthOnly?: boolean }} [opts] `sessionsRoot` = where the panel's sessions live (default `<home>/panel-sessions`); `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); `thisMonthOnly` = do not parse the spine of a run listed in another local month (the refusal check drops it anyway) unless its claim is still held
|
|
33
23
|
* @returns {Leg[]}
|
|
34
24
|
*/
|
|
35
25
|
export function readLegs(opts?: {
|
|
36
26
|
home?: string;
|
|
27
|
+
sessionsRoot?: string;
|
|
37
28
|
now?: () => number;
|
|
38
29
|
aboveRunid?: string;
|
|
30
|
+
thisMonthOnly?: boolean;
|
|
39
31
|
}): Leg[];
|
|
40
32
|
/**
|
|
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)
|
|
33
|
+
* @param {{ home?: string, sessionsRoot?: string, now?: () => number, aboveRunid?: string }} [opts] `aboveRunid`, `sessionsRoot`: see {@link readLegs}
|
|
34
|
+
* @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number, heldUsd: number, heldRuns: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's); `heldUsd` = what in-progress runs set aside beyond what they have spent (`reservedUsd - usd`), over `heldRuns` runs
|
|
43
35
|
*/
|
|
44
36
|
export function monthSpend(opts?: {
|
|
45
37
|
home?: string;
|
|
38
|
+
sessionsRoot?: string;
|
|
46
39
|
now?: () => number;
|
|
47
40
|
aboveRunid?: string;
|
|
48
41
|
}): {
|
|
@@ -50,18 +43,21 @@ export function monthSpend(opts?: {
|
|
|
50
43
|
atLeast: boolean;
|
|
51
44
|
runs: number;
|
|
52
45
|
reservedUsd: number;
|
|
46
|
+
heldUsd: number;
|
|
47
|
+
heldRuns: number;
|
|
53
48
|
};
|
|
54
49
|
/**
|
|
55
50
|
* The Money tab's figures: all-time and this-month spend, and per-provider breakdown
|
|
56
51
|
* (rows from `rowIdFor`; a run on no known row lands under its own provider name, never
|
|
57
52
|
* pooled into another row). Any leg with unknown spend makes the figure it belongs to
|
|
58
53
|
* 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
|
|
54
|
+
* @param {{ home?: string, sessionsRoot?: string, now?: () => number, rows?: readonly import('./providerrows.js').KeyRow[] }} [opts] `rows` = the Providers rows: when given, `tokensByRow` sums each row's tokens
|
|
60
55
|
* @returns {{ tokensByRow: Record<string, number>, total: {usd: number, atLeast: boolean, tokens: number}, month: {usd: number, atLeast: boolean, tokens: number},
|
|
61
56
|
* 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
57
|
*/
|
|
63
58
|
export function spendSummary(opts?: {
|
|
64
59
|
home?: string;
|
|
60
|
+
sessionsRoot?: string;
|
|
65
61
|
now?: () => number;
|
|
66
62
|
rows?: readonly import("./providerrows.js").KeyRow[];
|
|
67
63
|
}): {
|
|
@@ -97,6 +93,8 @@ export function spendSummary(opts?: {
|
|
|
97
93
|
* @property {number|null} leftUsd what is left this month (null = no limit set)
|
|
98
94
|
* @property {number|null} limitUsd the person's monthly limit (null = none)
|
|
99
95
|
* @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
|
|
96
|
+
* @property {number} [heldUsd] what run(s) in progress have set aside beyond their spend so far (present when a limit is set)
|
|
97
|
+
* @property {number} [heldRuns] how many runs hold it
|
|
100
98
|
*/
|
|
101
99
|
/**
|
|
102
100
|
* The person's monthly limit off a parsed config.json: key absent (or null) = no limit (null);
|
|
@@ -112,17 +110,19 @@ export function monthlyLimitOf(config: Record<string, any>, home?: string): numb
|
|
|
112
110
|
* number above 0 throws `ConfigError`.
|
|
113
111
|
* Compared in whole cents. Also throws `ConfigError` when config.json is unreadable — a gate
|
|
114
112
|
* 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)
|
|
113
|
+
* @param {{ capUsd: number, home?: string, sessionsRoot?: string, now?: () => number, aboveRunid?: string }} args `aboveRunid`: count only the claims listed above that run (a claim in progress)
|
|
116
114
|
* @returns {MonthlyRoom}
|
|
117
115
|
*/
|
|
118
|
-
export function checkMonthlyRoom({ capUsd, home, now, aboveRunid }: {
|
|
116
|
+
export function checkMonthlyRoom({ capUsd, home, now, aboveRunid, sessionsRoot }: {
|
|
119
117
|
capUsd: number;
|
|
120
118
|
home?: string;
|
|
119
|
+
sessionsRoot?: string;
|
|
121
120
|
now?: () => number;
|
|
122
121
|
aboveRunid?: string;
|
|
123
122
|
}): MonthlyRoom;
|
|
124
123
|
/**
|
|
125
|
-
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
|
|
124
|
+
* The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal). When run(s) in progress hold
|
|
125
|
+
* part of the limit, the same line says how much.
|
|
126
126
|
* @param {MonthlyRoom} room
|
|
127
127
|
* @returns {string|null} null when the run fits
|
|
128
128
|
*/
|
|
@@ -148,19 +148,21 @@ export function settleDeadClaims({ home, by, aboveRunid }: {
|
|
|
148
148
|
* it appends `{type:'released', reason:'refused'}` and reports the refusal, nothing spent. No limit set =
|
|
149
149
|
* no claim (`claimed: false`; the caller lists the run as it always did). An unreadable config, or a row
|
|
150
150
|
* that cannot be written, throws `ConfigError` — the gate refuses, never reads "no limit".
|
|
151
|
-
*
|
|
151
|
+
* A RESUMED leg (`leg` 2+, P5-R) claims the same way but appends a `leg-start` beside the run's one row
|
|
152
|
+
* (the row itself is added only when the list has none), and a refusal gives back THAT leg's claim.
|
|
153
|
+
* @param {{ row: import('./runlist.js').RunRow, capUsd: number, home?: string, now?: () => number, leg?: number }} args
|
|
152
154
|
* @returns {{ room: MonthlyRoom, claimed: boolean }}
|
|
153
155
|
*/
|
|
154
|
-
export function claimRun({ row, capUsd, home, now }: {
|
|
156
|
+
export function claimRun({ row, capUsd, home, now, leg }: {
|
|
155
157
|
row: import("./runlist.js").RunRow;
|
|
156
158
|
capUsd: number;
|
|
157
159
|
home?: string;
|
|
158
160
|
now?: () => number;
|
|
161
|
+
leg?: number;
|
|
159
162
|
}): {
|
|
160
163
|
room: MonthlyRoom;
|
|
161
164
|
claimed: boolean;
|
|
162
165
|
};
|
|
163
|
-
export { isLiveRunner };
|
|
164
166
|
export type Leg = {
|
|
165
167
|
/**
|
|
166
168
|
* when the run was listed
|
|
@@ -208,6 +210,10 @@ export type Leg = {
|
|
|
208
210
|
* the spine was missing/unreadable or the listed date was bad (spend unknown)
|
|
209
211
|
*/
|
|
210
212
|
unreadable: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* drafting spend of a panel session that never became a run (src/draftspend.js) — not a run: no wall, not counted in `runs`
|
|
215
|
+
*/
|
|
216
|
+
draft?: boolean | undefined;
|
|
211
217
|
};
|
|
212
218
|
export type MonthlyRoom = {
|
|
213
219
|
ok: boolean;
|
|
@@ -223,5 +229,16 @@ export type MonthlyRoom = {
|
|
|
223
229
|
* the month's spend is a floor, so `leftUsd` is a ceiling
|
|
224
230
|
*/
|
|
225
231
|
atLeast: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* what run(s) in progress have set aside beyond their spend so far (present when a limit is set)
|
|
234
|
+
*/
|
|
235
|
+
heldUsd?: number | undefined;
|
|
236
|
+
/**
|
|
237
|
+
* how many runs hold it
|
|
238
|
+
*/
|
|
239
|
+
heldRuns?: number | undefined;
|
|
226
240
|
};
|
|
227
|
-
import {
|
|
241
|
+
import { legSpend } from './ledger.js';
|
|
242
|
+
import { runSpend } from './ledger.js';
|
|
243
|
+
import { chainSpend } from './ledger.js';
|
|
244
|
+
export { legSpend, runSpend, chainSpend };
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
/** @returns {string} a fresh per-process token — never persisted, never logged */
|
|
2
2
|
export function mintToken(): string;
|
|
3
|
+
/**
|
|
4
|
+
* The ONE Host check: the request names this panel's own bind address. A page on another
|
|
5
|
+
* origin reaching the panel through a rebound DNS name sends that name as Host, so it fails here.
|
|
6
|
+
* @param {import('node:http').IncomingMessage} req
|
|
7
|
+
* @param {{port: number}} o
|
|
8
|
+
* @returns {{ok: true}|{ok: false, reason: string}}
|
|
9
|
+
*/
|
|
10
|
+
export function checkHostGuard(req: import("node:http").IncomingMessage, { port }: {
|
|
11
|
+
port: number;
|
|
12
|
+
}): {
|
|
13
|
+
ok: true;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
3
18
|
/**
|
|
4
19
|
* `true` when a request may reach ANY route in this file — the token header
|
|
5
20
|
* plus an Origin/Host that names this exact bind address. `req.headers`
|
|
@@ -25,7 +40,8 @@ export function checkHumanGuard(req: import("node:http").IncomingMessage, { toke
|
|
|
25
40
|
* deliberately has none of).
|
|
26
41
|
* @param {{ port: number, token: string, env?: Record<string,string|undefined>,
|
|
27
42
|
* sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
|
|
28
|
-
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string
|
|
43
|
+
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string,
|
|
44
|
+
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any} }} opts
|
|
29
45
|
*/
|
|
30
46
|
export function createAuthorRoutes(opts: {
|
|
31
47
|
port: number;
|
|
@@ -37,10 +53,31 @@ export function createAuthorRoutes(opts: {
|
|
|
37
53
|
jobsDir?: string;
|
|
38
54
|
fetchImpl?: typeof fetch;
|
|
39
55
|
home?: string;
|
|
56
|
+
startFrom?: {
|
|
57
|
+
get: (runid: string, o?: {
|
|
58
|
+
model?: string;
|
|
59
|
+
}) => any;
|
|
60
|
+
getImport: (id: string, o?: {
|
|
61
|
+
model?: string;
|
|
62
|
+
}) => any;
|
|
63
|
+
};
|
|
40
64
|
}): {
|
|
41
65
|
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
42
66
|
sessions: Map<string, any>;
|
|
43
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* The panel's OWN 2-decimal money render for a chat line — hamr's ruling
|
|
70
|
+
* 2026-09-28 (2nd addendum, panel money 2-decimals): a positive amount below
|
|
71
|
+
* a cent reads `<$0.01`, never `$0.00` (a real cost must never read as
|
|
72
|
+
* free). This is the server-side (Node, chat-text) sibling of
|
|
73
|
+
* `src/panel/index.html`'s own inline `panelMoney` — the two can't share
|
|
74
|
+
* code (one runs in the browser as inlined script, the other in Node), the
|
|
75
|
+
* same duplication `src/replay.js`'s `money()`/`index.html`'s `money()`
|
|
76
|
+
* already carry, so both apply the identical rule rather than drift.
|
|
77
|
+
* @param {number} n a non-negative dollar amount
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function panelMoney2(n: number): string;
|
|
44
81
|
/**
|
|
45
82
|
* THE ONLY function in this file (or anywhere in the panel) that spawns a
|
|
46
83
|
* run. Refuses unless the session is `prepared` AND the hash the request
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** @typedef {{id: string, label: string, status: 'running'|'done'|'failed', detail: string}} StepLine */
|
|
2
|
+
/** @param {StepLine[]} steps @param {string} id the LATEST line carrying this id (the list is a time-ordered log; an id can repeat) */
|
|
3
|
+
export function latestStep(steps: StepLine[], id: string): StepLine | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* The progress list is a chronological log of segments (hamr 2026-10-05): starting a different step closes the running
|
|
6
|
+
* line; a step that already has a line and starts again AFTER other steps gets a NEW line appended at the end (the
|
|
7
|
+
* earlier line keeps its status and detail). Existing lines are never moved. Re-starting the step that is still the
|
|
8
|
+
* LAST line just resumes that line.
|
|
9
|
+
* @param {StepLine[]} steps @param {string} id @param {string} [detail] (absent = keep what the line has)
|
|
10
|
+
*/
|
|
11
|
+
export function advanceSteps(steps: StepLine[], id: string, detail?: string): void;
|
|
1
12
|
/**
|
|
2
13
|
* `true` when `jobs/<name>.json` already exists in the repo's own jobs/ dir
|
|
3
14
|
* (resolved from THIS module's own on-disk location, never `process.cwd()`,
|
|
@@ -11,6 +22,67 @@
|
|
|
11
22
|
export function jobNameTaken(name: string, opts?: {
|
|
12
23
|
jobsDir?: string;
|
|
13
24
|
}): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Only the form's own fields, nothing else a request body might carry (`startFrom`, stray keys).
|
|
27
|
+
* @param {any} card
|
|
28
|
+
* @returns {Record<string, any>}
|
|
29
|
+
*/
|
|
30
|
+
export function cardFields(card: any): Record<string, any>;
|
|
31
|
+
/**
|
|
32
|
+
* The first LOCKED box on `card` that differs from what the signed workflow prefilled (`origin`), or `null` when
|
|
33
|
+
* every locked box is untouched. Blank and absent read alike; whitespace is not compared.
|
|
34
|
+
* @param {Record<string, any>} card the form as it was submitted
|
|
35
|
+
* @param {Record<string, any>} origin the form as the server prefilled it
|
|
36
|
+
* @returns {string|null} the offending field's label
|
|
37
|
+
*/
|
|
38
|
+
export function lockedFieldChanged(card: Record<string, any>, origin: Record<string, any>): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* The spec a reuse signs: a COPY of the origin's signed spec with ONLY the open fields set from the card —
|
|
41
|
+
* `writeScope` from Destination, `budgetUsd` from the $ cap, `maxWallMs` from the Time cap (absent when blank), and the
|
|
42
|
+
* worker (`provider`/`baseUrl`/`model`) from the Model Name chosen in Settings > Providers, spelled exactly as normal
|
|
43
|
+
* authoring spells it (`baseUrl` only when the row has one; `model` only when the Name is not the provider's default
|
|
44
|
+
* tier). A RUBRIC (soft-green) origin with no explicit `judge` first has its judge pinned to the ORIGIN's resolved judge
|
|
45
|
+
* identity — the judge must not change because the worker did (`resolveJobJudge`, the one spelling); an explicit
|
|
46
|
+
* `judge` is kept; a deterministic job has no judge to move. Source is not in a spec (it lives beside the run). The copy
|
|
47
|
+
* hashes to a NEW `jobSpecHash` and the SAME `workflowKey` — the caller checks the second after building.
|
|
48
|
+
* @param {any} originSpec
|
|
49
|
+
* @param {Record<string, any>} card
|
|
50
|
+
* @param {readonly import('../providerrows.js').KeyRow[]} [rows] the Settings rows; without them (or without `card.model`) the worker is left as signed
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
53
|
+
export function buildReuseSpec(originSpec: any, card: Record<string, any>, rows?: readonly import("../providerrows.js").KeyRow[]): any;
|
|
54
|
+
/**
|
|
55
|
+
* `$0` validation for a REUSE start: the signed job carries the goal, checks and guardrails, so those boxes are not
|
|
56
|
+
* required here — only what the new session itself needs: a Source, a Destination, a Model that is a Settings Name,
|
|
57
|
+
* and the cap. (The spec built from the open boxes is validated by the route with `validateJob`.)
|
|
58
|
+
* @param {any} card
|
|
59
|
+
* @param {{rows?: readonly import('../providerrows.js').KeyRow[]}} [opts]
|
|
60
|
+
* @returns {{ok: true}|{ok: false, error: string}}
|
|
61
|
+
*/
|
|
62
|
+
export function validateReuseCard(card: any, opts?: {
|
|
63
|
+
rows?: readonly import("../providerrows.js").KeyRow[];
|
|
64
|
+
}): {
|
|
65
|
+
ok: true;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
72
|
+
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
73
|
+
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function nonRepoSourceMessage(kind: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
|
|
79
|
+
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
80
|
+
* source that does not peek as a repo) keeps today's behaviour: its Destination is an absolute output directory.
|
|
81
|
+
* @param {string} destination
|
|
82
|
+
* @param {boolean} isRepoLike `looksLikeRepoSource(card.source)`
|
|
83
|
+
* @returns {string|null} the plain refusal, or null when the Destination is fine
|
|
84
|
+
*/
|
|
85
|
+
export function repoDestinationProblem(destination: string, isRepoLike: boolean): string | null;
|
|
14
86
|
/**
|
|
15
87
|
* `$0` validation of the job card fields the server must check BEFORE it
|
|
16
88
|
* creates a session or spends anything — never inside the async pipeline,
|
|
@@ -36,8 +108,9 @@ export function validateJobCard(card: any, opts?: {
|
|
|
36
108
|
* @param {any} card the validated job card (see {@link validateJobCard})
|
|
37
109
|
* @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
|
|
38
110
|
* scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
|
|
39
|
-
* prepareSigningFn?: Function}} [deps]
|
|
40
|
-
* just above where each is read, below.
|
|
111
|
+
* prepareSigningFn?: Function, reuse?: {spec: any, workflowKey: string}}} [deps] `scout`..`prepareSigningFn` are
|
|
112
|
+
* TEST SEAMS ONLY — see the note just above where each is read, below. `reuse` (Reuse workflow) is NOT a seam: the
|
|
113
|
+
* caller (`/api/author/start`) sets it only when the server's own same-job rule held.
|
|
41
114
|
* @returns {any} the session object
|
|
42
115
|
*/
|
|
43
116
|
export function createSession(card: any, deps?: {
|
|
@@ -50,4 +123,38 @@ export function createSession(card: any, deps?: {
|
|
|
50
123
|
confirmGenerate?: Function;
|
|
51
124
|
authorFn?: Function;
|
|
52
125
|
prepareSigningFn?: Function;
|
|
126
|
+
reuse?: {
|
|
127
|
+
spec: any;
|
|
128
|
+
workflowKey: string;
|
|
129
|
+
};
|
|
53
130
|
}): any;
|
|
131
|
+
/**
|
|
132
|
+
* THE PROGRESS LIST's one table (hamr 2026-10-04, "one place showing all"): every pipeline step the left Chat panel
|
|
133
|
+
* lists, in the order they first appear, each with a stable id and a plain code-owned label. A step is one line that
|
|
134
|
+
* updates in place (running -> done or failed with its reason); the thread carries only chat turns.
|
|
135
|
+
* @type {Readonly<Record<string, string>>}
|
|
136
|
+
*/
|
|
137
|
+
export const STEP_LABELS: Readonly<Record<string, string>>;
|
|
138
|
+
/**
|
|
139
|
+
* The library's own `onPhase(name, ...)` calls (`src/authorjob.js`/`src/authorflow.js`) -> the step each one advances.
|
|
140
|
+
* A phase with no entry leaves the current step as it was.
|
|
141
|
+
* @type {Readonly<Record<string, keyof typeof STEP_LABELS>>}
|
|
142
|
+
*/
|
|
143
|
+
export const PHASE_STEP: Readonly<Record<string, keyof typeof STEP_LABELS>>;
|
|
144
|
+
/** the form's fields, in card order — what `card.json` stores, verbatim (P5 item 3) */
|
|
145
|
+
export const CARD_FIELDS: readonly string[];
|
|
146
|
+
/**
|
|
147
|
+
* REUSE WORKFLOW (hamr 2026-10-03, replaces P5 item 3's same-job rule; Model opened 2026-10-04): a reuse card has exactly
|
|
148
|
+
* FIVE open boxes — Source, Destination, Model, $ cap, Time cap — and every other box is the signed workflow, shown greyed and never
|
|
149
|
+
* editable. The page greys them; THIS is what refuses (the server never trusts the page). Changing any locked box
|
|
150
|
+
* is a different job: Clear the card, which drafts.
|
|
151
|
+
*/
|
|
152
|
+
export const REUSE_OPEN_FIELDS: readonly string[];
|
|
153
|
+
/** every card field that is NOT open on a reuse card, in card order */
|
|
154
|
+
export const REUSE_LOCKED_FIELDS: readonly string[];
|
|
155
|
+
export type StepLine = {
|
|
156
|
+
id: string;
|
|
157
|
+
label: string;
|
|
158
|
+
status: "running" | "done" | "failed";
|
|
159
|
+
detail: string;
|
|
160
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand, normalise and `realpath` a requested folder. Never throws.
|
|
3
|
+
* @param {unknown} input the raw `path` (absent/blank = the person's home folder)
|
|
4
|
+
* @param {string} userHome the home folder (`~`)
|
|
5
|
+
* @returns {{ok: true, requested: string, path: string}|{ok: false, error: string}}
|
|
6
|
+
*/
|
|
7
|
+
export function resolveFsPath(input: unknown, userHome: string): {
|
|
8
|
+
ok: true;
|
|
9
|
+
requested: string;
|
|
10
|
+
path: string;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The exported job bundles below `dir` (a `realpath` already), searched DOWN at most SEARCH_DEPTH levels. Symlinks are
|
|
17
|
+
* never followed, `node_modules`, `.git` and every dot-folder are skipped, a bundle is never descended into, an
|
|
18
|
+
* unreadable folder is skipped silently, and the search is capped (folders visited, results). Only names are read.
|
|
19
|
+
* @param {string} dir
|
|
20
|
+
* @returns {{entries: {name: string, path: string, bundle: true}[], visited: number, stopped: boolean, truncated: boolean}}
|
|
21
|
+
*/
|
|
22
|
+
export function findBundles(dir: string): {
|
|
23
|
+
entries: {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
bundle: true;
|
|
27
|
+
}[];
|
|
28
|
+
visited: number;
|
|
29
|
+
stopped: boolean;
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** @param {string} [home] @returns {string} `<home>/imports.jsonl` */
|
|
33
|
+
export function importsPath(home?: string): string;
|
|
34
|
+
/** the stable id of an imported folder (its real path, hashed — never the path itself in a URL) */
|
|
35
|
+
export function importId(dir: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Record one import: ONE row per real folder. Any earlier row for the same folder is dropped and the new one is
|
|
38
|
+
* written, so re-importing updates the entry instead of adding a second. The rewrite goes through a temp file + rename.
|
|
39
|
+
* @param {string|undefined} home
|
|
40
|
+
* @param {{at: string, dir: string, job: string, bundleHash: string}} row
|
|
41
|
+
*/
|
|
42
|
+
export function recordImport(home: string | undefined, row: {
|
|
43
|
+
at: string;
|
|
44
|
+
dir: string;
|
|
45
|
+
job: string;
|
|
46
|
+
bundleHash: string;
|
|
47
|
+
}): void;
|
|
48
|
+
/**
|
|
49
|
+
* The imported list: the LATEST row per folder (a re-import supersedes), newest first. A malformed line is skipped.
|
|
50
|
+
* @param {string} [home]
|
|
51
|
+
* @returns {{at: string, dir: string, job: string, bundleHash: string, id: string}[]}
|
|
52
|
+
*/
|
|
53
|
+
export function readImports(home?: string): {
|
|
54
|
+
at: string;
|
|
55
|
+
dir: string;
|
|
56
|
+
job: string;
|
|
57
|
+
bundleHash: string;
|
|
58
|
+
id: string;
|
|
59
|
+
}[];
|
|
60
|
+
/**
|
|
61
|
+
* Re-read one imported folder NOW and say how it stands against the import: `ok`, `changed` (the bundle's own
|
|
62
|
+
* hash, a tampered file or an unreadable part differ from what was imported — shown red as "changed since
|
|
63
|
+
* import"), or `missing` (the folder is gone). Never throws.
|
|
64
|
+
* @param {{dir: string, bundleHash: string}} row
|
|
65
|
+
* @returns {{status: 'ok'|'changed'|'missing', statusText: string|null, reds: string[], bundle: ReturnType<typeof readBundle>}}
|
|
66
|
+
*/
|
|
67
|
+
export function bundleStatus(row: {
|
|
68
|
+
dir: string;
|
|
69
|
+
bundleHash: string;
|
|
70
|
+
}): {
|
|
71
|
+
status: "ok" | "changed" | "missing";
|
|
72
|
+
statusText: string | null;
|
|
73
|
+
reds: string[];
|
|
74
|
+
bundle: ReturnType<typeof readBundle>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The `bundle` half of an import view: the exported history (the registry rows the bundle shipped), the approval
|
|
78
|
+
* on THIS machine (`blessing.json`, written by the first green run here), nothing about the folder's other files.
|
|
79
|
+
* @param {ReturnType<typeof readBundle>} bundle
|
|
80
|
+
*/
|
|
81
|
+
export function bundleHistoryAndApproval(bundle: ReturnType<typeof readBundle>): {
|
|
82
|
+
history: {
|
|
83
|
+
greens: number;
|
|
84
|
+
reds: number;
|
|
85
|
+
total: number;
|
|
86
|
+
recent: {
|
|
87
|
+
at: any;
|
|
88
|
+
runid: any;
|
|
89
|
+
outcome: any;
|
|
90
|
+
costUsd: any;
|
|
91
|
+
}[];
|
|
92
|
+
};
|
|
93
|
+
approved: boolean;
|
|
94
|
+
approvedText: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The import routes' handler. `describeSpec(spec)` is the server's own spec reader (the SAME fact-readers the Job
|
|
98
|
+
* tab uses, so an imported job and a run read alike): `{checkType, goal, success, guardrails, model, budgetUsd,
|
|
99
|
+
* maxWallMs}`.
|
|
100
|
+
* @param {{ port: number, token: string, home?: string, userHome?: string,
|
|
101
|
+
* describeSpec: (spec: any) => any }} opts
|
|
102
|
+
*/
|
|
103
|
+
export function createImportRoutes(opts: {
|
|
104
|
+
port: number;
|
|
105
|
+
token: string;
|
|
106
|
+
home?: string;
|
|
107
|
+
userHome?: string;
|
|
108
|
+
describeSpec: (spec: any) => any;
|
|
109
|
+
}): {
|
|
110
|
+
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
111
|
+
viewOf: (row: {
|
|
112
|
+
id: string;
|
|
113
|
+
at: string;
|
|
114
|
+
dir: string;
|
|
115
|
+
job: string;
|
|
116
|
+
bundleHash: string;
|
|
117
|
+
}) => any;
|
|
118
|
+
};
|
|
119
|
+
/** the longest path the routes will look at (bytes) */
|
|
120
|
+
export const MAX_PATH_LEN: 4096;
|
|
121
|
+
/** how many levels below the current folder the bundle search looks */
|
|
122
|
+
export const SEARCH_DEPTH: 3;
|
|
123
|
+
/** the most folders one search visits; past it `stopped: true` */
|
|
124
|
+
export const MAX_VISITED: 5000;
|
|
125
|
+
/** the most bundles one search returns; past it `truncated: true` */
|
|
126
|
+
export const MAX_RESULTS: 200;
|
|
127
|
+
/** The ONE spelling of the plain-words line for a changed import (Run tab, reuse refusal). The list tag stays the short statusText. */
|
|
128
|
+
export const IMPORT_CHANGED_LINE: "files changed since you imported it \u2014 Reuse is off. Import it again if the change was yours.";
|
|
129
|
+
import { readBundle } from '../bundle.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** the id an imported run has in `/api/runs/<id>` @param {string} importId @param {string} runid */
|
|
2
|
+
export function importedRunId(importId: string, runid: string): string;
|
|
3
|
+
/** @param {string} id @returns {{importId: string, runid: string}|null} */
|
|
4
|
+
export function parseImportedRunId(id: string): {
|
|
5
|
+
importId: string;
|
|
6
|
+
runid: string;
|
|
7
|
+
} | null;
|
|
8
|
+
/**
|
|
9
|
+
* The spine path of `<dir>/runs/<runid>`, or null when it is not a plain, bounded file in plain folders: `runs/` and
|
|
10
|
+
* the run folder must be real directories (a link is refused), the spine a regular file under the size bound, and a
|
|
11
|
+
* tool log beside it (either spelling the replay reader looks for) must not be a link or oversize either.
|
|
12
|
+
* @param {string} dir the bundle folder
|
|
13
|
+
* @param {string} runid
|
|
14
|
+
* @returns {string|null}
|
|
15
|
+
*/
|
|
16
|
+
export function safeSpinePath(dir: string, runid: string): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* The run folders of a bundle that hold a readable spine, newest spine first (bounded).
|
|
19
|
+
* @param {string} dir
|
|
20
|
+
* @returns {{runid: string, spine: string, mtimeMs: number}[]}
|
|
21
|
+
*/
|
|
22
|
+
export function bundleRuns(dir: string): {
|
|
23
|
+
runid: string;
|
|
24
|
+
spine: string;
|
|
25
|
+
mtimeMs: number;
|
|
26
|
+
}[];
|
|
27
|
+
/**
|
|
28
|
+
* When the run started: its `job-start` record's `ts`, else the spine file's mtime (an ISO string). Never throws.
|
|
29
|
+
* @param {string} spine
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function spineStartedAt(spine: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The newest run in the bundle whose spine ended green (`green` / `already-green`, the same two the run page's Ended
|
|
35
|
+
* block calls green). A spine that cannot be read is skipped, never fatal.
|
|
36
|
+
* @param {string} dir
|
|
37
|
+
* @returns {{runid: string, spine: string, at: string}|null}
|
|
38
|
+
*/
|
|
39
|
+
export function latestGreenRun(dir: string): {
|
|
40
|
+
runid: string;
|
|
41
|
+
spine: string;
|
|
42
|
+
at: string;
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* The newest green version across a bundle's bridges (by `greenAt`), with the history row of the same run (it
|
|
46
|
+
* carries whether the spend figure is complete). A malformed bridge or version is skipped.
|
|
47
|
+
* @param {any[]} bridges
|
|
48
|
+
* @returns {{bridge: any, version: any, historyRow: any|null}|null}
|
|
49
|
+
*/
|
|
50
|
+
export function latestBridgeGreen(bridges: any[]): {
|
|
51
|
+
bridge: any;
|
|
52
|
+
version: any;
|
|
53
|
+
historyRow: any | null;
|
|
54
|
+
} | null;
|
|
55
|
+
/**
|
|
56
|
+
* The run detail of the bridge's green version, in the SAME shape `getRunDetail` returns (so the page's one run
|
|
57
|
+
* renderer paints it), plus `fromBridge: true`. Only what the bridge carries is filled; every other field is null
|
|
58
|
+
* ("not recorded" on the page), never 0: no tool counts, no cache, no per-step figures, no close stages' numbers.
|
|
59
|
+
* @param {{bridge: any, version: any, historyRow: any|null}} g
|
|
60
|
+
* @param {{job: string, checkType: string, checkTypeTitle: string|null, model: string|null, budgetUsd: number|null}} facts
|
|
61
|
+
* @returns {any}
|
|
62
|
+
*/
|
|
63
|
+
export function bridgeRunDetail(g: {
|
|
64
|
+
bridge: any;
|
|
65
|
+
version: any;
|
|
66
|
+
historyRow: any | null;
|
|
67
|
+
}, facts: {
|
|
68
|
+
job: string;
|
|
69
|
+
checkType: string;
|
|
70
|
+
checkTypeTitle: string | null;
|
|
71
|
+
model: string | null;
|
|
72
|
+
budgetUsd: number | null;
|
|
73
|
+
}): any;
|
|
74
|
+
/** the most run folders one bundle is searched through (newest first) */
|
|
75
|
+
export const MAX_RUN_DIRS: 40;
|
|
76
|
+
/** the biggest spine / tool-log file that is read (bytes) */
|
|
77
|
+
export const MAX_RUN_FILE_BYTES: number;
|