bareloop 0.27.0 → 0.29.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/types/replay.d.ts CHANGED
@@ -1,3 +1,41 @@
1
+ /**
2
+ * `[startTs, endTs]` (epoch ms; `endTs` possibly `Infinity`) bounding the
3
+ * gate-audit rows that actually belong to ONE run's own spine. A sidecar
4
+ * file can be SHARED across several runs whose spines happen to reuse the
5
+ * same filename (measured directly on a real archived patient —
6
+ * pulselog-person-live-2, run mu2p83go: its sidecar carries rows from 7
7
+ * distinct `run_id`s spanning 06:56Z..13:25Z on one day, but this run's own
8
+ * `job-start`..`job-end` window is 13:19:40Z..13:25:48Z; every row before
9
+ * that window belongs to an earlier, unrelated run sharing the sidecar
10
+ * path). No `step`/`run_id` field on a gate-audit row is a reliable enough
11
+ * key on its own (see this file's header) — bounding by `ts` against this
12
+ * run's own `job-start`/`job-end` is the only honest scope.
13
+ *
14
+ * `startTs` is this run's own `job-start.ts` (`-Infinity` when absent —
15
+ * never a guessed lower bound). `endTs` is `job-end.ts` when a `job-end`
16
+ * was actually reached, else `Infinity` (a died/still-running run — never
17
+ * clamped to the spine's own last record's `ts`, which would wrongly
18
+ * exclude a real tool call the gate logged just AFTER the spine's own last
19
+ * record landed: spine and gate-audit are written by different seams and
20
+ * are not guaranteed to interleave exactly).
21
+ *
22
+ * ONE shared owner for this rule (F195): `replayRun` (this file) uses it to
23
+ * scope every audit row it hands to `runBehaviour`, so `bareloop replay`
24
+ * and every caller of `replayRun`/`replayOne` (including the panel,
25
+ * `src/panel/server.js`) read the same, honest figure — never two places
26
+ * independently re-deriving (and risking drifting from) the same rule.
27
+ * @param {{ts?: string}|null} [jobStart]
28
+ * @param {{ts?: string}|null} [jobEnd]
29
+ * @returns {{startTs: number, endTs: number}}
30
+ */
31
+ export function auditWindow(jobStart?: {
32
+ ts?: string;
33
+ } | null, jobEnd?: {
34
+ ts?: string;
35
+ } | null): {
36
+ startTs: number;
37
+ endTs: number;
38
+ };
1
39
  /**
2
40
  * Reconstruct one run's whole story from its spine + gate-audit. Pure: no
3
41
  * IO, no spine writes, no verdict. Malformed/non-object entries in either
@@ -18,16 +56,26 @@
18
56
  *
19
57
  * @param {any[]} spineEvents parsed records from `u-<id>.jsonl`
20
58
  * @param {any[]} [auditEvents] parsed records from the sibling `-gate-audit.jsonl`
21
- * @param {{runId?: string|null}} [opts] `runId`: caller-supplied (the spine
22
- * carries no run-id field of its own — see the file header)
59
+ * @param {{runId?: string|null, auditAvailable?: boolean}} [opts] `runId`:
60
+ * caller-supplied (the spine carries no run-id field of its own — see the
61
+ * file header). `auditAvailable` (default `true`, back-compat: every
62
+ * direct caller in this codebase that hands a literal `[]` here means a
63
+ * REAL, known-empty audit, not "unknown") — set `false` only when the
64
+ * caller could not even determine whether a gate-audit sidecar exists (no
65
+ * file found on disk, or deliberately not read). When `false`, every
66
+ * tool-call figure this function derives (`behaviour`, each occurrence's
67
+ * `toolCalls`) reads `null` (unknown), never `0` — doctrine: unknown is
68
+ * reported as unknown, never rendered as zero.
23
69
  */
24
- export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
70
+ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, auditAvailable }?: {
25
71
  runId?: string | null;
72
+ auditAvailable?: boolean;
26
73
  }): {
27
74
  runId: string | null;
28
75
  job: any;
29
76
  verdictType: any;
30
77
  model: any;
78
+ provider: any;
31
79
  code: {
32
80
  version: any;
33
81
  sha: any;
@@ -71,14 +119,51 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
71
119
  diffUsd: number;
72
120
  } | null;
73
121
  timelineKind: "steps" | "iterations";
122
+ scoutPlan: {
123
+ rounds: number;
124
+ toolCalls: number | null;
125
+ wallMs: number | null;
126
+ spentUsd: number | null;
127
+ unpricedRounds: number;
128
+ tripped: {
129
+ category: any;
130
+ detail: string | null;
131
+ } | null;
132
+ transportRetries: {
133
+ count: number;
134
+ label: string;
135
+ error: string | null;
136
+ } | null;
137
+ scoutRounds: number;
138
+ planRounds: number;
139
+ } | null;
74
140
  steps: {
75
141
  checks: {
76
142
  passed: number;
77
143
  failed: number;
78
144
  };
79
145
  treeChanged: boolean;
146
+ attempts: {
147
+ n: number;
148
+ iteration: number | null;
149
+ outcome: "green" | "red";
150
+ }[];
151
+ attemptWindows: {
152
+ n: number;
153
+ iteration: number | null;
154
+ outcome: "green" | "red";
155
+ startSeq: number;
156
+ endSeq: number;
157
+ startTs: number | null;
158
+ endTs: number | null;
159
+ exitEvalDetail: string | null;
160
+ }[];
161
+ stepStartSeq: any;
162
+ stepEndSeq: any;
163
+ stepStartTs: number | null;
164
+ stepEndTs: number | null;
80
165
  rounds: number;
81
- toolCalls: number;
166
+ toolCalls: number | null;
82
167
  wallMs: number | null;
83
168
  spentUsd: number | null;
84
169
  unpricedRounds: number;
@@ -96,8 +181,12 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
96
181
  outcome: any;
97
182
  }[];
98
183
  iterations: {
184
+ windowStartSeq: any;
185
+ windowEndSeq: any;
186
+ windowStartTs: number | null;
187
+ windowEndTs: number | null;
99
188
  rounds: number;
100
- toolCalls: number;
189
+ toolCalls: number | null;
101
190
  wallMs: number | null;
102
191
  spentUsd: number | null;
103
192
  unpricedRounds: number;
@@ -118,6 +207,15 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
118
207
  stages: any;
119
208
  } | null;
120
209
  }[];
210
+ fixLoop: {
211
+ attempts: any[];
212
+ rounds: any;
213
+ toolCalls: any;
214
+ wallMs: any;
215
+ spentUsd: any;
216
+ unpricedRounds: any;
217
+ } | null;
218
+ parts: any[];
121
219
  replans: any;
122
220
  close: {
123
221
  source: "close-verdict" | "outer-close";
@@ -137,7 +235,7 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
137
235
  uniquePaths: number;
138
236
  repeats: number;
139
237
  repeatPct: number | null;
140
- };
238
+ } | null;
141
239
  memoryCache: any;
142
240
  skipped: number;
143
241
  };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Parse one JSONL file into records, tolerant of a malformed line (counted
3
+ * in `skipped`, never thrown on). Mirrors `scripts/behaviour-readout.mjs`'s
4
+ * own parse.
5
+ * @param {string} file
6
+ * @returns {{records: any[], skipped: number}}
7
+ */
8
+ export function parseJsonl(file: string): {
9
+ records: any[];
10
+ skipped: number;
11
+ };
12
+ /**
13
+ * `true` when `name` (a bare filename, not a path) is a gate-audit or lag
14
+ * sidecar by NAME convention — the one filename rule that holds across the
15
+ * whole archive.
16
+ * @param {string} name
17
+ */
18
+ export function isSidecarByName(name: string): boolean;
19
+ /**
20
+ * `true` when `records` carries at least one `job-start` (present on every
21
+ * real spine checked, first record or not — a reuse/battery run nests one or
22
+ * more ordinary `runJob` calls inside it) or `run-start` (ralph's own inner
23
+ * loop marker, present on most but not all real spines).
24
+ * @param {any[]} records
25
+ */
26
+ export function looksLikeSpine(records: any[]): boolean;
27
+ /**
28
+ * `<name>.jsonl` → `<name>-gate-audit.jsonl` in the same directory, and the
29
+ * bare run id for display — the run id lives only in the filename (no spine
30
+ * record carries it; see `src/replay.js`'s header). A leading `u-` (the
31
+ * run-u convention) is stripped; every other prefix (`battery-A1-`,
32
+ * `reuse-`, …) is kept verbatim, since it is the only thing distinguishing
33
+ * that run from its siblings in the same directory.
34
+ * @param {string} spinePath
35
+ * @returns {{runId: string, auditPath: string|null}}
36
+ */
37
+ export function resolveSiblings(spinePath: string): {
38
+ runId: string;
39
+ auditPath: string | null;
40
+ };
41
+ /**
42
+ * Read one run's spine (+ its gate-audit sidecar, when present) off disk and
43
+ * hand both to {@link replayRun}. Read-only, $0, mints no verdict.
44
+ * @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
45
+ * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean}} [opts]
46
+ * `preParsedSpine`: avoids a second parse of the same file when the caller
47
+ * ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
48
+ * `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
49
+ * directory listing never opens the gate-audit sidecar at all —
50
+ * `summarizeForAllLine` reads none of the fields that sidecar feeds inside
51
+ * `replayRun`, so reading every run's sidecar in a directory listing was
52
+ * pure unused I/O, multiplied by every spine found.
53
+ * @returns {ReturnType<typeof replayRun>}
54
+ */
55
+ export function replayOne(spinePath: string, opts?: {
56
+ preParsedSpine?: {
57
+ records: any[];
58
+ skipped: number;
59
+ };
60
+ skipAudit?: boolean;
61
+ }): ReturnType<typeof replayRun>;
62
+ /**
63
+ * List every spine found directly in `dir` (not recursive — same as the
64
+ * script this replaces): one entry per `.jsonl` file that is not a sidecar
65
+ * by name, each either `{kind:'spine', row}` ({@link summarizeForAllLine}'s
66
+ * row) or `{kind:'not-a-spine', name}` when the file's content doesn't look
67
+ * like a spine. Feeds `formatAllLines` (`src/replay.js`) for the printed
68
+ * table — this function does no formatting of its own, only IO + detection.
69
+ * @param {string} dir
70
+ * @returns {Array<{kind: 'spine', row: ReturnType<typeof summarizeForAllLine>}|{kind: 'not-a-spine', name: string}>}
71
+ */
72
+ export function listSpines(dir: string): Array<{
73
+ kind: "spine";
74
+ row: ReturnType<typeof summarizeForAllLine>;
75
+ } | {
76
+ kind: "not-a-spine";
77
+ name: string;
78
+ }>;
79
+ /**
80
+ * Read a bundle's `history.jsonl` (one `bareloop run` row per line, written
81
+ * by `appendHistory` in `src/bundle.js`) — the single reader for this file,
82
+ * replacing `src/cli.js`'s own hand-rolled `readFileSync`/split. Tolerant of
83
+ * a malformed line the same way {@link parseJsonl} is (counted in
84
+ * `skipped`, never thrown on).
85
+ * @param {string} file absolute path to `history.jsonl`
86
+ * @returns {{rows: any[], skipped: number}}
87
+ */
88
+ export function readHistoryLog(file: string): {
89
+ rows: any[];
90
+ skipped: number;
91
+ };
92
+ import { replayRun } from './replay.js';
93
+ import { summarizeForAllLine } from './replay.js';
package/types/reuse.d.ts CHANGED
@@ -234,7 +234,7 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
234
234
  * abandoned attempt twice in a chain of resumes.
235
235
  *
236
236
  * @param {any[]} events the dead spine's events, parsed, in file order
237
- * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: string[]}} [opts]
237
+ * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: readonly string[]}} [opts]
238
238
  * `deathAt`: when the process is judged to have died (the watchdog's kill record,
239
239
  * which is later and better evidence than the last event). Defaults to the last
240
240
  * event's own timestamp — the last sign of life there is.
@@ -246,7 +246,7 @@ export function selectBridge({ registry, job, ask, provider, pinned, shortlist,
246
246
  export function readResume(events: any[], { deathAt, direct, resumableOutcomes }?: {
247
247
  deathAt?: number | null;
248
248
  direct?: boolean;
249
- resumableOutcomes?: string[];
249
+ resumableOutcomes?: readonly string[];
250
250
  }): any;
251
251
  /**
252
252
  * The AGE gate for a paused checkpoint — the sibling of `resumeTreeGate`, and it
package/types/run.d.ts CHANGED
@@ -143,7 +143,7 @@
143
143
  * a manufactured-fixation probe). `true` is the ON/experimental arm; the
144
144
  * default is the OFF arm. (Shell-owned seam, same doctrine as the provider
145
145
  * binding.)
146
- * @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
146
+ * @param {{decision: string, text?: string|null}|null} [opts.humanRuling] N4 — the SIGNER's
147
147
  * answer at a hitl pause (accept | rerun <text> | pause), carried by the leg that
148
148
  * RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
149
149
  * ordinary run; never authored and never defaulted (a defaulted answer to "is this
@@ -219,7 +219,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
219
219
  layerRoot?: boolean | undefined;
220
220
  humanRuling?: {
221
221
  decision: string;
222
- text?: string;
222
+ text?: string | null;
223
223
  } | null | undefined;
224
224
  heldRuling?: {
225
225
  decision: string;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `~/.config/bareloop` (or the injected override) — the one home directory
3
+ * for both the keys file (PRD §7d, not this module's concern) and the run
4
+ * list.
5
+ * @param {string} [override] test seam; a caller in production never passes it
6
+ * @returns {string}
7
+ */
8
+ export function runlistHome(override?: string): string;
9
+ /**
10
+ * `<home>/runs.jsonl`.
11
+ * @param {string} [home]
12
+ * @returns {string}
13
+ */
14
+ export function runlistPath(home?: string): string;
15
+ /**
16
+ * @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill' }} RunRow
17
+ */
18
+ /**
19
+ * Append one row to the run list, mkdir -p'ing the home directory first.
20
+ * IDEMPOTENT BY RUNID: a row already carrying `row.runid` is never
21
+ * duplicated (a resume, or a re-run of `backfill`, must not double-list one
22
+ * run) — the file is read once first to check.
23
+ *
24
+ * Perms: the home directory is created `0700` and the file kept `0600`
25
+ * (chmod'd after every append, in case it pre-existed with wider perms) —
26
+ * the same "outside any repo, keys-file-adjacent" posture PRD §7d states for
27
+ * `.env` in this same directory, even though this file carries no secret
28
+ * itself.
29
+ *
30
+ * THROWS on any IO failure (permission denied, disk full, …) — the caller
31
+ * (a live run) is required to catch this and continue the run rather than
32
+ * let a list-append failure abort paid work (hamr's rule: a panel list must
33
+ * never block real work). This function itself does not swallow, so a
34
+ * direct caller (e.g. `backfillRuns`, a $0 path) sees the real error.
35
+ * @param {RunRow} row
36
+ * @param {{ home?: string }} [opts]
37
+ * @returns {{ appended: boolean, reason?: 'duplicate-runid' }}
38
+ */
39
+ export function appendRun(row: RunRow, opts?: {
40
+ home?: string;
41
+ }): {
42
+ appended: boolean;
43
+ reason?: "duplicate-runid";
44
+ };
45
+ /**
46
+ * Tolerant reader — reuses {@link parseJsonl} (never a second hand-rolled
47
+ * parser). An absent file reads as an empty list, not an error: a fresh
48
+ * install has never run `appendRun` yet.
49
+ * @param {{ home?: string }} [opts]
50
+ * @returns {{ rows: RunRow[], skipped: number }}
51
+ */
52
+ export function readRunList(opts?: {
53
+ home?: string;
54
+ }): {
55
+ rows: RunRow[];
56
+ skipped: number;
57
+ };
58
+ /**
59
+ * `bareloop runs backfill <dir>` — recursively scan `dir` (bounded to
60
+ * {@link MAX_BACKFILL_DEPTH} levels, never into `node_modules`/`.git`,
61
+ * never following a symlinked directory) for archived spine files — both
62
+ * the free-standing layout (e.g. `<dir>/<x>/u-<id>.jsonl`, at any depth) and
63
+ * the bundle layout (`<dir>/.../runs/<runid>/spine.jsonl`) — and add one row
64
+ * per spine whose RESOLVED ABSOLUTE PATH is not already in the list.
65
+ *
66
+ * F197: dedup is by spine path, never by `runidForSpine`'s own derived
67
+ * runid. A spine carrying no `runid` field falls back to a filename-derived
68
+ * id (e.g. `run` for a `run.jsonl` with no runid inside) — a SECOND such
69
+ * file elsewhere (same basename, different directory: not a rare shape,
70
+ * `bareloop-patients/quickstart-proof/run.jsonl` is a real example) is a
71
+ * DIFFERENT run, not a duplicate, and dedup-by-runid would have silently
72
+ * dropped it. When the derived runid collides with one already in the list
73
+ * (pre-existing or added earlier in this same pass), the row's OWN runid is
74
+ * disambiguated (`<base>~2`, `<base>~3`, …) so every row keeps a runid the
75
+ * panel's `/api/runs/:runid` lookup can resolve unambiguously — see
76
+ * {@link RUNID_RE} in `src/panel/server.js`, updated to allow `~`.
77
+ *
78
+ * IDEMPOTENT: running twice adds nothing the second time (the resolved-path
79
+ * set already contains every spine this pass would otherwise re-add). Never
80
+ * silent about what it found: every candidate `.jsonl` is bucketed into
81
+ * exactly one of added / already listed / skipped (not a spine, or
82
+ * unreadable).
83
+ * @param {string} dir
84
+ * @param {{ home?: string }} [opts]
85
+ * @returns {{ added: number, alreadyListed: number, skipped: number, addedRows: RunRow[] }}
86
+ */
87
+ export function backfillRuns(dir: string, opts?: {
88
+ home?: string;
89
+ }): {
90
+ added: number;
91
+ alreadyListed: number;
92
+ skipped: number;
93
+ addedRows: RunRow[];
94
+ };
95
+ /**
96
+ * One printable line per row for `bareloop runs` (no subcommand): `job
97
+ * (runid) · date · spine path`, with `file missing` appended when the row's
98
+ * `spine` no longer exists on disk (an archive moved/pruned since the row
99
+ * was written — never silently listed as if it were still there).
100
+ * @param {RunRow} row
101
+ * @returns {string}
102
+ */
103
+ export function formatRunRow(row: RunRow): string;
104
+ export const MAX_BACKFILL_DEPTH: 6;
105
+ export type RunRow = {
106
+ at: string;
107
+ runid: string;
108
+ job: string;
109
+ spine: string;
110
+ patient: string | null;
111
+ via: "run-u" | "bundle" | "backfill";
112
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reset a patient to its frozen seed and drop its `.litectx` store.
3
+ *
4
+ * @param {string} wd absolute path to the patient's tree (a git repo with >=1 commit)
5
+ * @param {string} seed the frozen seed commit
6
+ * @returns {{ head: string, storeRemoved: boolean }} `head` is the short sha the tree
7
+ * now stands at; `storeRemoved` says whether a store was actually there to remove
8
+ * (so a caller can print the truth rather than a claim).
9
+ */
10
+ export function coldReset(wd: string, seed: string): {
11
+ head: string;
12
+ storeRemoved: boolean;
13
+ };
14
+ /**
15
+ * F186 — the patient's own `.gitignore` denies `*.jsonl`, so `coldReset`'s
16
+ * `git clean -fd` (no `-x`) never removes a `gate-audit.jsonl` already
17
+ * sitting at the tree root — it can only be there from an EARLIER run
18
+ * against this same patient (an authoring scout, or a prior worker run)
19
+ * that never got archived out. Left in place, THIS run's own Gate (which
20
+ * writes to that same path) would append to it, and the end-of-run rename
21
+ * that claims the whole file as "this run's own audit" would silently
22
+ * carry every earlier run's rows too — a harness slicing a shared append-
23
+ * only log has to account for every writer inside its window.
24
+ *
25
+ * Call this AFTER `coldReset` and BEFORE this run's own Gate ever opens: at
26
+ * that point a `gate-audit.jsonl` sitting in the tree is provably not this
27
+ * run's, because coldReset just proved the tree is unable to have produced
28
+ * one since the seed reset. NOT for a resume — a halted run's own gate
29
+ * audit already in the tree it resumes into is that SAME run's prior leg,
30
+ * not a stranger's; callers on the resume path must not call this.
31
+ *
32
+ * @param {string} wd absolute path to the patient's tree, just cold-reset
33
+ * @param {string} spineDir absolute path to this run's spine directory
34
+ * @param {string} runid this run's own id (the same one every other spine
35
+ * file for this run is named after)
36
+ * @returns {string|null} the path the stale file was moved to, or `null`
37
+ * when there was nothing to move
38
+ */
39
+ export function moveStaleGateAudit(wd: string, spineDir: string, runid: string): string | null;