bareloop 0.27.0 → 0.28.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/src/planrun.js CHANGED
@@ -932,7 +932,7 @@ ${scoutBlob || '(no scout notes)'}`;
932
932
  * it the deterministic name would collide with that very branch and the collision walk
933
933
  * would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
934
934
  * path. A recorded branch that no longer exists is a STOP, never a fresh start.
935
- * @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
935
+ * @param {{decision: string, text?: string|null}|null} [opts.humanRuling] N4 — the SIGNER's
936
936
  * answer at a hitl pause (2026-08-12 §1, re-cut 2026-08-17: accept | rerun <text> | pause), carried in
937
937
  * by the runner on the leg that RESUMES a paused run. Absent on every ordinary run and
938
938
  * on the leg that pauses; it is never authored, never defaulted, and never inferred.
@@ -0,0 +1,153 @@
1
+ // PANEL-BUILD.md P0 — the spine/gate-audit READ side. Lifted out of
2
+ // `scripts/run-replay.mjs` (verbatim logic, moved so it is callable
3
+ // in-process rather than only as a script) plus one more reader
4
+ // (`readHistoryLog`) for `bareloop history`'s bundle-registry file. This
5
+ // module is the IO layer ONLY: it resolves paths, parses JSONL tolerantly
6
+ // (a malformed line is counted and skipped, never thrown on), and hands
7
+ // parsed records to `src/replay.js`'s pure `replayRun`/`formatReplay`/
8
+ // `summarizeForAllLine`/`formatAllLines` — it never recomputes anything
9
+ // those already derive (PANEL-BUILD.md P0: "feed replay.js, never
10
+ // duplicate it").
11
+ //
12
+ // NAME-AGNOSTIC spine detection (coordinator fix, 2026-08-25, carried over
13
+ // verbatim from the script this replaces): the patient corpus does not agree
14
+ // on a filename convention — `u-<id>.jsonl` (run-u), `battery-A1-<id>.jsonl`
15
+ // / `l2accept-L1-<id>.jsonl` (batteries), `layer-r-probe-P1-<id>.jsonl`,
16
+ // `types-screen-C-<id>.jsonl`, `reuse-<id>.jsonl` (first record
17
+ // `reuse-start`), `job2-<id>.jsonl` all measured across the real archive. A
18
+ // spine is therefore identified by CONTENT, never by a naming pattern: any
19
+ // `*.jsonl` that is not a gate-audit/lag sidecar BY NAME
20
+ // (`*-gate-audit.jsonl`, `*.lag.jsonl` — the one convention that IS
21
+ // universal, because every spine resolves its own audit sibling this same
22
+ // way) and whose records include at least one `job-start` or `run-start` is
23
+ // a spine. A `.jsonl` that matches neither test is reported as
24
+ // `<name> not-a-spine` — never silently dropped, never silently misread as
25
+ // an empty/unknown run.
26
+
27
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
28
+ import { basename, dirname, join } from 'node:path';
29
+ import { replayRun, summarizeForAllLine } from './replay.js';
30
+
31
+ /**
32
+ * Parse one JSONL file into records, tolerant of a malformed line (counted
33
+ * in `skipped`, never thrown on). Mirrors `scripts/behaviour-readout.mjs`'s
34
+ * own parse.
35
+ * @param {string} file
36
+ * @returns {{records: any[], skipped: number}}
37
+ */
38
+ export function parseJsonl(file) {
39
+ const raw = readFileSync(file, 'utf8');
40
+ const lines = raw.split('\n').filter((l) => l.trim() !== '');
41
+ /** @type {any[]} */
42
+ const records = [];
43
+ let skipped = 0;
44
+ for (const line of lines) {
45
+ try {
46
+ records.push(JSON.parse(line));
47
+ } catch {
48
+ skipped += 1;
49
+ }
50
+ }
51
+ return { records, skipped };
52
+ }
53
+
54
+ /**
55
+ * `true` when `name` (a bare filename, not a path) is a gate-audit or lag
56
+ * sidecar by NAME convention — the one filename rule that holds across the
57
+ * whole archive.
58
+ * @param {string} name
59
+ */
60
+ export function isSidecarByName(name) {
61
+ return name.endsWith('-gate-audit.jsonl') || name.endsWith('.lag.jsonl');
62
+ }
63
+
64
+ /**
65
+ * `true` when `records` carries at least one `job-start` (present on every
66
+ * real spine checked, first record or not — a reuse/battery run nests one or
67
+ * more ordinary `runJob` calls inside it) or `run-start` (ralph's own inner
68
+ * loop marker, present on most but not all real spines).
69
+ * @param {any[]} records
70
+ */
71
+ export function looksLikeSpine(records) {
72
+ return records.some((r) => r && (r.type === 'job-start' || r.type === 'run-start'));
73
+ }
74
+
75
+ /**
76
+ * `<name>.jsonl` → `<name>-gate-audit.jsonl` in the same directory, and the
77
+ * bare run id for display — the run id lives only in the filename (no spine
78
+ * record carries it; see `src/replay.js`'s header). A leading `u-` (the
79
+ * run-u convention) is stripped; every other prefix (`battery-A1-`,
80
+ * `reuse-`, …) is kept verbatim, since it is the only thing distinguishing
81
+ * that run from its siblings in the same directory.
82
+ * @param {string} spinePath
83
+ * @returns {{runId: string, auditPath: string|null}}
84
+ */
85
+ export function resolveSiblings(spinePath) {
86
+ const dir = dirname(spinePath);
87
+ const base = basename(spinePath);
88
+ const stem = base.replace(/\.jsonl$/, '');
89
+ const runId = stem.startsWith('u-') ? stem.slice(2) : stem;
90
+ const auditPath = join(dir, `${stem}-gate-audit.jsonl`);
91
+ return { runId, auditPath: existsSync(auditPath) ? auditPath : null };
92
+ }
93
+
94
+ /**
95
+ * Read one run's spine (+ its gate-audit sidecar, when present) off disk and
96
+ * hand both to {@link replayRun}. Read-only, $0, mints no verdict.
97
+ * @param {string} spinePath absolute or cwd-relative path to a `.jsonl` spine
98
+ * @param {{preParsedSpine?: {records: any[], skipped: number}, skipAudit?: boolean}} [opts]
99
+ * `preParsedSpine`: avoids a second parse of the same file when the caller
100
+ * ({@link listSpines}) already read it once to run {@link looksLikeSpine}.
101
+ * `skipAudit` (PR #23 review item 5, 2026-08-26, carried over): a
102
+ * directory listing never opens the gate-audit sidecar at all —
103
+ * `summarizeForAllLine` reads none of the fields that sidecar feeds inside
104
+ * `replayRun`, so reading every run's sidecar in a directory listing was
105
+ * pure unused I/O, multiplied by every spine found.
106
+ * @returns {ReturnType<typeof replayRun>}
107
+ */
108
+ export function replayOne(spinePath, opts = {}) {
109
+ const { preParsedSpine, skipAudit = false } = opts;
110
+ const { runId, auditPath } = resolveSiblings(spinePath);
111
+ const spine = preParsedSpine ?? parseJsonl(spinePath);
112
+ const audit = (!skipAudit && auditPath) ? parseJsonl(auditPath) : { records: [], skipped: 0 };
113
+ const summary = replayRun(spine.records, audit.records, { runId });
114
+ summary.skipped += spine.skipped + audit.skipped;
115
+ return summary;
116
+ }
117
+
118
+ /**
119
+ * List every spine found directly in `dir` (not recursive — same as the
120
+ * script this replaces): one entry per `.jsonl` file that is not a sidecar
121
+ * by name, each either `{kind:'spine', row}` ({@link summarizeForAllLine}'s
122
+ * row) or `{kind:'not-a-spine', name}` when the file's content doesn't look
123
+ * like a spine. Feeds `formatAllLines` (`src/replay.js`) for the printed
124
+ * table — this function does no formatting of its own, only IO + detection.
125
+ * @param {string} dir
126
+ * @returns {Array<{kind: 'spine', row: ReturnType<typeof summarizeForAllLine>}|{kind: 'not-a-spine', name: string}>}
127
+ */
128
+ export function listSpines(dir) {
129
+ const candidates = readdirSync(dir)
130
+ .filter((f) => f.endsWith('.jsonl') && !isSidecarByName(f))
131
+ .sort();
132
+ return candidates.map((f) => {
133
+ const path = join(dir, f);
134
+ const spine = parseJsonl(path);
135
+ if (!looksLikeSpine(spine.records)) return { kind: /** @type {const} */ ('not-a-spine'), name: f };
136
+ const summary = replayOne(path, { preParsedSpine: spine, skipAudit: true });
137
+ return { kind: /** @type {const} */ ('spine'), row: summarizeForAllLine(summary) };
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Read a bundle's `history.jsonl` (one `bareloop run` row per line, written
143
+ * by `appendHistory` in `src/bundle.js`) — the single reader for this file,
144
+ * replacing `src/cli.js`'s own hand-rolled `readFileSync`/split. Tolerant of
145
+ * a malformed line the same way {@link parseJsonl} is (counted in
146
+ * `skipped`, never thrown on).
147
+ * @param {string} file absolute path to `history.jsonl`
148
+ * @returns {{rows: any[], skipped: number}}
149
+ */
150
+ export function readHistoryLog(file) {
151
+ const { records, skipped } = parseJsonl(file);
152
+ return { rows: records, skipped };
153
+ }
package/src/reuse.js CHANGED
@@ -805,7 +805,7 @@ function readGradeSeed(seen) {
805
805
  * abandoned attempt twice in a chain of resumes.
806
806
  *
807
807
  * @param {any[]} events the dead spine's events, parsed, in file order
808
- * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: string[]}} [opts]
808
+ * @param {{deathAt?: number|null, direct?: boolean, resumableOutcomes?: readonly string[]}} [opts]
809
809
  * `deathAt`: when the process is judged to have died (the watchdog's kill record,
810
810
  * which is later and better evidence than the last event). Defaults to the last
811
811
  * event's own timestamp — the last sign of life there is.
package/src/run.js CHANGED
@@ -204,7 +204,7 @@ async function primitiveSmoke(workdir) {
204
204
  * a manufactured-fixation probe). `true` is the ON/experimental arm; the
205
205
  * default is the OFF arm. (Shell-owned seam, same doctrine as the provider
206
206
  * binding.)
207
- * @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
207
+ * @param {{decision: string, text?: string|null}|null} [opts.humanRuling] N4 — the SIGNER's
208
208
  * answer at a hitl pause (accept | rerun <text> | pause), carried by the leg that
209
209
  * RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
210
210
  * ordinary run; never authored and never defaulted (a defaulted answer to "is this
@@ -0,0 +1,73 @@
1
+ // THE COLD RESET, in ONE place.
2
+ //
3
+ // `run-u.mjs` has always owned this: a cold run starts at the frozen seed with the
4
+ // `.litectx` store removed, because a run that inherits the previous run's edits — or
5
+ // the previous run's isolate-verb memory (stash/remember persist in the store) — is
6
+ // measuring the wrong thing. The reuse rung's OFF arm and every cold contrast depend
7
+ // on it.
8
+ //
9
+ // It moved here the day a BATTERY driver needed to rehearse it (the read-shim Phase 2
10
+ // battery's `--dry-run` has to prove the patient really does go cold between rows).
11
+ // The alternative was a second spelling of `git reset --hard` in the battery, and two
12
+ // spellings of "cold" is exactly how one driver's rows come to start from a different
13
+ // tree than the other's — the blind-instrument class, at the patient.
14
+ //
15
+ // Nothing here decides anything and nothing here is arbiter territory: it is the
16
+ // mechanical preparation of a tree, and both callers print what it did.
17
+ import { execFileSync } from 'node:child_process';
18
+ import {
19
+ rmSync, existsSync, renameSync,
20
+ } from 'node:fs';
21
+ import { join } from 'node:path';
22
+
23
+ /**
24
+ * Reset a patient to its frozen seed and drop its `.litectx` store.
25
+ *
26
+ * @param {string} wd absolute path to the patient's tree (a git repo with >=1 commit)
27
+ * @param {string} seed the frozen seed commit
28
+ * @returns {{ head: string, storeRemoved: boolean }} `head` is the short sha the tree
29
+ * now stands at; `storeRemoved` says whether a store was actually there to remove
30
+ * (so a caller can print the truth rather than a claim).
31
+ */
32
+ export function coldReset(wd, seed) {
33
+ const git = (/** @type {string[]} */ a) => execFileSync('git', ['-C', wd, ...a], { encoding: 'utf8' }).trim();
34
+ git(['reset', '--hard', seed]);
35
+ git(['clean', '-fd']);
36
+ const store = join(wd, '.litectx');
37
+ const storeRemoved = existsSync(store);
38
+ rmSync(store, { recursive: true, force: true });
39
+ return { head: git(['rev-parse', '--short', 'HEAD']), storeRemoved };
40
+ }
41
+
42
+ /**
43
+ * F186 — the patient's own `.gitignore` denies `*.jsonl`, so `coldReset`'s
44
+ * `git clean -fd` (no `-x`) never removes a `gate-audit.jsonl` already
45
+ * sitting at the tree root — it can only be there from an EARLIER run
46
+ * against this same patient (an authoring scout, or a prior worker run)
47
+ * that never got archived out. Left in place, THIS run's own Gate (which
48
+ * writes to that same path) would append to it, and the end-of-run rename
49
+ * that claims the whole file as "this run's own audit" would silently
50
+ * carry every earlier run's rows too — a harness slicing a shared append-
51
+ * only log has to account for every writer inside its window.
52
+ *
53
+ * Call this AFTER `coldReset` and BEFORE this run's own Gate ever opens: at
54
+ * that point a `gate-audit.jsonl` sitting in the tree is provably not this
55
+ * run's, because coldReset just proved the tree is unable to have produced
56
+ * one since the seed reset. NOT for a resume — a halted run's own gate
57
+ * audit already in the tree it resumes into is that SAME run's prior leg,
58
+ * not a stranger's; callers on the resume path must not call this.
59
+ *
60
+ * @param {string} wd absolute path to the patient's tree, just cold-reset
61
+ * @param {string} spineDir absolute path to this run's spine directory
62
+ * @param {string} runid this run's own id (the same one every other spine
63
+ * file for this run is named after)
64
+ * @returns {string|null} the path the stale file was moved to, or `null`
65
+ * when there was nothing to move
66
+ */
67
+ export function moveStaleGateAudit(wd, spineDir, runid) {
68
+ const staleAudit = join(wd, 'gate-audit.jsonl');
69
+ if (!existsSync(staleAudit)) return null;
70
+ const preFile = join(spineDir, `pre-${runid}-gate-audit.jsonl`);
71
+ renameSync(staleAudit, preFile);
72
+ return preFile;
73
+ }