bareloop 0.7.0 → 0.9.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/ralph.js CHANGED
@@ -285,6 +285,21 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
285
285
  };
286
286
  }
287
287
 
288
+ /**
289
+ * The staged gap's header — WHICH wall the close stopped at, in front of that
290
+ * wall's own output. A worker handed three stages' worth of output cannot tell
291
+ * which one it must satisfy, and `src/trend.js` parses this exact line to bucket
292
+ * a grade per stage (its `STAGE_HEADER`).
293
+ *
294
+ * Exported for the one reason `GAP_KEEP_TRIM_MARKER` is: a SECOND executor now
295
+ * renders staged gaps (`src/declaredclose.js`, the declared close), and two
296
+ * spellings of this line would be two instruments — the reader would silently
297
+ * stop bucketing whichever one drifted. Behaviour here is unchanged; this is the
298
+ * same template, named.
299
+ * @param {string} name @param {string} gap
300
+ */
301
+ export const stageGap = (name, gap) => `close stage "${name}" failed:\n${gap}`;
302
+
288
303
  /**
289
304
  * Run a STAGED close (PRD v1.28) — an ordered list of named command stages, run
290
305
  * in order until one renders a verdict that is not `satisfied`. The return is
@@ -310,7 +325,7 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
310
325
  * @param {{timeoutMs?: number, cwd?: string}} [opts]
311
326
  * @returns {Promise<any>} runClose's verdict shape, plus `stage` and `stages`
312
327
  */
313
- export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } = {}) {
328
+ export async function runStages(stages, redact = (s) => s, /** @type {{timeoutMs?: number, cwd?: string}} */ { timeoutMs, cwd } = {}) {
314
329
  const ran = [];
315
330
  let last;
316
331
  for (const st of stages) {
@@ -326,9 +341,9 @@ export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } =
326
341
  ...r,
327
342
  stage: st.name,
328
343
  stages: ran,
329
- // The gap says WHICH wall, then the wall's own output. A worker handed
330
- // three stages' worth of output cannot tell which one it must satisfy.
331
- ...(r.gap ? { gap: `close stage "${st.name}" failed:\n${r.gap}` } : {}),
344
+ // The gap says WHICH wall, then the wall's own output (`stageGap`, the
345
+ // one spelling — the declared executor renders the same header).
346
+ ...(r.gap ? { gap: stageGap(st.name, r.gap) } : {}),
332
347
  };
333
348
  }
334
349
  }
@@ -359,10 +374,17 @@ export const GAP_KEEP_TRIM_MARKER = 'more elided by the';
359
374
  * `keepPattern` is set (F28), lines of the WHOLE stream matching it are also
360
375
  * carried in a clearly-delimited block between head and tail — capped — so the
361
376
  * failing-test names buried in the middle still reach the worker.
377
+ * Exported for the ONE reason `GAP_KEEP_TRIM_MARKER` above is: a second consumer
378
+ * that needs a gap bounded must reuse THIS envelope, not spell its own. A private
379
+ * copy would be a second truncation scheme, and the two would drift on exactly the
380
+ * property that matters — whether the red lines survive, and whether the trim
381
+ * announces itself (F28: silent truncation is the disease this function cures).
382
+ * The close path is unchanged: `runClose` still calls it with the stage's own
383
+ * `gapKeep`, byte-identically.
362
384
  * @param {string} s the (redacted) close output
363
385
  * @param {string} [keepPattern] regex source; matching lines survive the elision
364
386
  */
365
- function boundGap(s, keepPattern) {
387
+ export function boundGap(s, keepPattern) {
366
388
  if (s.length <= GAP_HEAD + GAP_TAIL + 100) return s;
367
389
  const head = s.slice(0, GAP_HEAD);
368
390
  const tail = s.slice(-GAP_TAIL);
package/src/reuse.js CHANGED
@@ -48,7 +48,7 @@ import { createHash } from 'node:crypto';
48
48
  import { createRequire } from 'node:module';
49
49
  import { loadRegistry, saveBridge, appendGreen, appendRed, mintBridge, registryExists } from './bridges.js';
50
50
  import { renderListing, selectionPrompt } from './selection.js';
51
- import { stageClose } from './plan.js';
51
+ import { closeStagesOf } from './plan.js';
52
52
  import { MIN_WALL_MS, jobSpecHash } from './job.js';
53
53
  import { isObj, isNonEmptyString } from './validate.js';
54
54
  import { readGrade } from './trend.js';
@@ -498,6 +498,30 @@ function readStepCheckpoint(seen) {
498
498
  };
499
499
  }
500
500
 
501
+ /**
502
+ * The WORK BRANCH the dead leg created (PRD v1.57 §3), read off its own `work-branch`
503
+ * record — the one place the runner writes it down.
504
+ *
505
+ * It is read rather than re-derived, and that is the whole point. `workBranchName` is
506
+ * deterministic from the signed spec, so a restart computing it fresh would get the very
507
+ * name its predecessor already took and the collision walk would hand it
508
+ * `<name>-2` — a brand-new branch standing beside the work the resume exists to
509
+ * continue. The declared-fold precedent (`priorSpentUsd`, `priorReplans`): what the
510
+ * chain already did is DECLARED on the spine and inherited, never recomputed.
511
+ *
512
+ * The LAST record wins. A leg emits exactly one, but a window can reach back through an
513
+ * abandoned attempt of the same try, and the newest is the branch the work is actually
514
+ * sitting on.
515
+ * @param {any[]} seen the window's own events, in file order
516
+ * @returns {string|null} null on a spine older than the record, or a kill before 0c
517
+ */
518
+ function readWorkBranch(seen) {
519
+ for (let i = seen.length - 1; i >= 0; i -= 1) {
520
+ if (seen[i].type === 'work-branch' && isNonEmptyString(seen[i].branch)) return String(seen[i].branch);
521
+ }
522
+ return null;
523
+ }
524
+
501
525
  /**
502
526
  * The dead leg's CLOSE GRADES, in order — the trend baselines a resumed leg
503
527
  * inherits (PRD v1.46 §3, and hamr's approved #2: *a resumed run's trend judges the
@@ -594,7 +618,11 @@ function readGradeSeed(seen) {
594
618
  * **The declared fold, not a re-derivation.** A restarted try's own `try-start`
595
619
  * carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
596
620
  * resume adds only its own new rounds. Re-deriving from the whole file would bill an
597
- * abandoned attempt twice.
621
+ * abandoned attempt twice. The REPLAN ledger (`priorReplans`/`priorReplanGrantUsed`)
622
+ * folds on this same mechanism and deliberately not on `readGradeSeed`'s: a grade seed
623
+ * feeds a readout, so its documented one-leg-per-resume shortening can only under-claim
624
+ * a direction, while an under-claimed CEILING is a refilled allowance and every kill
625
+ * would buy one (PRD v1.12 — the replan bound is the RUN's).
598
626
  *
599
627
  * **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
600
628
  * returned left no event at all — no `worker-round`, no `selection-result` — so money it
@@ -688,6 +716,19 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
688
716
  // declaration existed, or a try nobody resumed — both are honestly zero.
689
717
  declaredRounds: !('priorRounds' in ev) ? 0
690
718
  : (typeof ev.priorRounds === 'number' && Number.isFinite(ev.priorRounds) && ev.priorRounds >= 0 ? ev.priorRounds : null),
719
+ // the REPLAN ledger this attempt was already carrying, on the same declared-fold
720
+ // mechanism and with the same `> 0` belt as the money. It has to fold this way
721
+ // rather than the grade seed's way: `readGradeSeed`'s chain shortens by one leg
722
+ // per resume, which under-claims — harmless for a readout, and for a CEILING an
723
+ // under-claim IS a refilled allowance, one per kill (PRD v1.12).
724
+ declaredReplans: typeof ev.priorReplans === 'number' && Number.isFinite(ev.priorReplans) && ev.priorReplans > 0 ? Math.floor(ev.priorReplans) : 0,
725
+ // the F85-C latch rides BESIDE the count, never derived from it: a chain can have
726
+ // spent the ordinary ceiling with the arbiter's one extra still unearned, and
727
+ // deriving one from the other would collapse those two states into one
728
+ declaredGrantUsed: ev.priorReplanGrantUsed === true,
729
+ // this window's OWN replans, counted from the records the runner emits — the same
730
+ // `replan` event a human reads, so the ledger and the log cannot disagree
731
+ windowReplans: 0, windowGrantUsed: false,
691
732
  roundsUsd: 0, roundsComplete: true, r1Written: false, jobEnd: null,
692
733
  /** @type {any[]} the window's own events, so the SAME `readTry` the live loop uses
693
734
  * reads a graded-but-unrecorded try — never a second reader spelling it differently */
@@ -745,6 +786,16 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
745
786
  open.jobEnd = ev;
746
787
  } else if (ev.type === 'bridge-write') {
747
788
  open.r1Written = true;
789
+ } else if (ev.type === 'replan') {
790
+ // ONE record per replan the runner drafts, so counting them IS the leg's ledger
791
+ // — never re-derived from `plan-accepted` (a resumed leg emits one of those for a
792
+ // plan it RELOADED, which would read as a redraft nobody paid for).
793
+ open.windowReplans += 1;
794
+ // the arbiter stamps `granted` only on the extra it granted past the ordinary
795
+ // ceiling, and stamps nothing on the ordinary one — so PRESENCE is the whole
796
+ // signal, and reading it by presence keeps this reader honest about a reading it
797
+ // does not itself make (never re-deciding whether the trend deserved it).
798
+ if (ev.granted !== undefined) open.windowGrantUsed = true;
748
799
  } else if (ev.type === 'try-end') {
749
800
  completed.push({ ...ev, seq: undefined, ts: undefined, inherited: true });
750
801
  if (open.bridge) tried.push(open.bridge);
@@ -815,6 +866,30 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
815
866
  // restart already re-graded that tree. Counting both would enter the same
816
867
  // reading twice and flatten a chain that moved.
817
868
  grades: readGradeSeed(open.seen),
869
+ // the WORK BRANCH this leg's uncommitted work is sitting on (v1.57 §3). Read
870
+ // from THIS window first and then from the newest abandoned window of the same
871
+ // try, on the checkpoint's own rule and for the checkpoint's own reason: the
872
+ // branch — like the plan — is a fact about the TREE, and the tree survives an
873
+ // attempt that died before recording anything of its own. Null is the honest
874
+ // answer for a leg killed before 0c (or a spine older than the record); the
875
+ // resume then takes the cold path and creates one, which is correct, because a
876
+ // leg that never branched has no work to strand.
877
+ branch: readWorkBranch(open.seen)
878
+ ?? abandoned.filter((w) => w.n === open.n).map((w) => readWorkBranch(w.seen)).filter(Boolean).at(-1)
879
+ ?? null,
880
+ // the REPLAN ledger, folded like the money one line below and deliberately
881
+ // NOT like the grades one line above. A grade seed feeds a READOUT and its
882
+ // documented shortening chain can only under-claim a direction; this feeds a
883
+ // BOUND, where under-claiming is a refilled allowance and every kill would buy
884
+ // one (PRD v1.12: unlimited replanning launders thrash as adaptation). So it
885
+ // reads the declared fold plus THIS window's own records — the same arithmetic
886
+ // as `priorSpentUsd`, which is what makes a resume of a resume inherit leg 1.
887
+ //
888
+ // The abandoned windows are not read, for the money's exact reason: the
889
+ // restart DECLARED what it inherited, so counting the window it abandoned
890
+ // would charge the chain twice for one leg's replans.
891
+ replans: open.declaredReplans + open.windowReplans,
892
+ replanGrantUsed: open.declaredGrantUsed || open.windowGrantUsed,
818
893
  priorSpentUsd: open.declaredSpentUsd + open.roundsUsd,
819
894
  // F83 — was that fold EXACT? Three sources, because `runJob` decides the same
820
895
  // question from more than this window can see (src/run.js: `!unpriced &&
@@ -1017,7 +1092,7 @@ export async function runReuse(opts) {
1017
1092
  const runid = opts.runid ?? Date.now().toString(36);
1018
1093
  const resume = isObj(opts.resume) ? /** @type {any} */ (opts.resume) : null;
1019
1094
  /** a try that inherits nothing: the ordinary, non-resumed attempt */
1020
- const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]) };
1095
+ const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]), replans: 0, replanGrantUsed: false };
1021
1096
 
1022
1097
  /** @type {any[]} */
1023
1098
  const tries = [];
@@ -1113,7 +1188,7 @@ export async function runReuse(opts) {
1113
1188
 
1114
1189
  /** the shared per-try execution: run the job, read the spine, write the box (R1).
1115
1190
  * @param {any|null} bridge @param {number} n
1116
- * @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[]}} [prior]
1191
+ * @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[], replans: number, replanGrantUsed: boolean}} [prior]
1117
1192
  * RESUME — what a KILLED attempt of this same try already consumed, and what it
1118
1193
  * already measured. Money and wall are FOLDED into the attempt (`runJob`'s own ledger
1119
1194
  * and clock start partly spent) rather than shrinking the caps, so the signed per-try
@@ -1121,14 +1196,23 @@ export async function runReuse(opts) {
1121
1196
  * way but on the ROW alone (nothing enforces a round bound here), and `null` — an
1122
1197
  * attempt whose turns could not be counted — is reported, never folded. `grades` are
1123
1198
  * the close readings that leg recorded, handed on as the run trend's BASELINES: they
1124
- * feed the HALT READOUT alone and spend none of this attempt's bounds.
1199
+ * feed the HALT READOUT alone and spend none of this attempt's bounds. `replans` /
1200
+ * `replanGrantUsed` are the opposite kind of inheritance and sit here for that
1201
+ * contrast: they SPEND a bound (the plan flow's replan ceiling), because that ceiling
1202
+ * is the RUN's by doctrine and a fresh one per kill is the creep v1.12 forbids.
1125
1203
  * @param {any} [seed] RESUME (v2) — WHERE that attempt was killed (`readResume`'s
1126
1204
  * `restart.seed`): the plan it accepted and the steps it finished. The fold says how
1127
1205
  * much of the try's allowance is gone; this says how much of its WORK is done, so the
1128
1206
  * restart re-pays for neither the scout, the draft, nor a finished step. Null is the
1129
1207
  * scout/draft death, where nothing paid is re-payable and the try simply restarts.
1208
+ * @param {string|null} [branch] RESUME (v1.57 §3) — the WORK BRANCH that attempt's work
1209
+ * is sitting on (`readResume`'s `restart.branch`). The name is derived from the SIGNED
1210
+ * spec and is therefore the same on every leg, so a restart that recomputed it would
1211
+ * collide with its own predecessor and be handed a fresh `-2` beside the work it came
1212
+ * back for. Declared and inherited, exactly like the money and the replan ledger. Null
1213
+ * is the cold path, and also the leg killed before it ever branched.
1130
1214
  * @returns {Promise<any>} the try row */
1131
- const runTry = async (bridge, n, prior = NO_PRIOR, seed = null) => {
1215
+ const runTry = async (bridge, n, prior = NO_PRIOR, seed = null, branch = null) => {
1132
1216
  const mode = bridge ? 'bridge' : 'cold';
1133
1217
  const name = bridge ? bridge.name : null;
1134
1218
  const resumed = prior.spentUsd > 0 || prior.wallMs > 0;
@@ -1142,6 +1226,15 @@ export async function runReuse(opts) {
1142
1226
  // means "a spine older than this field", and a reader that cannot tell that from a
1143
1227
  // predecessor's honest unknown would launder the unknown into a zero (F6).
1144
1228
  ...(resumed ? { priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, priorRounds: prior.rounds, resumedFrom: resume?.fromRunid ?? runid } : {}),
1229
+ // the REPLAN ledger, declared on the same record and for the same reason as the
1230
+ // money: the next reader adds only its own window's `replan` records to this
1231
+ // number, so a chain of resumes folds once instead of restarting the ceiling at
1232
+ // whichever leg it happens to be reading (PRD v1.12 — the bound is the RUN's).
1233
+ // Declared independently of `resumed`, which is a MONEY/WALL test: a leg can
1234
+ // replan without buying a round the fold would notice, and gating the ledger on
1235
+ // the wallet would drop it exactly there. Only when there is one to state — a
1236
+ // decorative zero is indistinguishable from a leg nobody folded.
1237
+ ...(prior.replans > 0 ? { priorReplans: prior.replans, priorReplanGrantUsed: prior.replanGrantUsed } : {}),
1145
1238
  // WHERE it picks up, on the row's own record: a try that restarts at step 3 of 4 is
1146
1239
  // a materially different attempt from one that restarts at its beginning, and a
1147
1240
  // reader must not have to infer which happened from the absence of a scout.
@@ -1173,7 +1266,16 @@ export async function runReuse(opts) {
1173
1266
  // restarted leg would report `flat` on a run that was converging. Only when there
1174
1267
  // ARE grades: an empty array is the cold path, and the cold path stays identical.
1175
1268
  ...(prior.grades.length ? { resumeGrades: prior.grades } : {}),
1269
+ // and the replan LEDGER. Same omission class as the two above, with a sharper
1270
+ // cost: a grade seam that never arrives makes a readout say `flat`, while a
1271
+ // ceiling that never arrives is spent by the dead leg and fresh in this one —
1272
+ // a bound the operator signed, widened by a kill (PRD v1.12).
1273
+ ...(prior.replans > 0 ? { resumeReplans: { count: prior.replans, grantUsed: prior.replanGrantUsed } } : {}),
1176
1274
  ...(seed ? { resumeSeed: seed } : {}),
1275
+ // …and WHERE its work is. Same omission class as the three folds above, with its
1276
+ // own cost: a branch that never arrives is not a wrong number but a SECOND branch,
1277
+ // and the leg would carry on beside the tree it was resumed to continue.
1278
+ ...(branch ? { resumeBranch: branch } : {}),
1177
1279
  bridge,
1178
1280
  });
1179
1281
  } catch (e) {
@@ -1233,7 +1335,7 @@ export async function runReuse(opts) {
1233
1335
  name: name ?? job.job,
1234
1336
  goal: job.goal,
1235
1337
  specHash: trySpecHash,
1236
- closeStageNames: (stageClose(job.close) ?? []).map((/** @type {any} */ s) => s.name),
1338
+ closeStageNames: (closeStagesOf(job) ?? []).map((/** @type {any} */ s) => s.name),
1237
1339
  toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
1238
1340
  };
1239
1341
  row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
@@ -1366,6 +1468,19 @@ export async function runReuse(opts) {
1366
1468
  decision: 'The plan flow cannot execute this job\'s close — it runs commands whose exit codes are truth. Nothing was judged, and no workflow can be graded by a close that will not run, so the next try and the cold leg would refuse identically for $0 apiece.',
1367
1469
  options: ['restate the close as a staged (or single predicate) close — a spec edit, whose new hash needs re-approval', 'wait for the verdict-classes rung'],
1368
1470
  },
1471
+ 'branch-red': {
1472
+ // NOT re-filed, for `interpreter-red`'s reason plus one of its own: the runner
1473
+ // already escalated it at the fault's own site, and the fault is the OPERATOR's
1474
+ // state (a patient that is not a git checkout, a crowded branch namespace),
1475
+ // never a library failing — the ledger excludes it for exactly that.
1476
+ refile: false,
1477
+ decision: 'The run could not create its own work branch, and no job runs on the branch it was handed (PRD v1.57 §3). That is a property of the PATIENT, not of any stored workflow: every further try and the cold leg would refuse identically, for $0 apiece.',
1478
+ options: [
1479
+ 'make the patient a git checkout with at least one commit',
1480
+ 'free the work-branch name (delete or rename the stale branches this run collided with)',
1481
+ 'abandon the run',
1482
+ ],
1483
+ },
1369
1484
  'interpreter-red': {
1370
1485
  // NOT re-filed as an escalation. `interpreter-red` is one of the categories the
1371
1486
  // ledger CLASSIFIES — it aims a runtime-red at a library — and runJob already
@@ -1446,6 +1561,12 @@ export async function runReuse(opts) {
1446
1561
  // this run make progress" and "is this loop out of ideas" are two questions,
1447
1562
  // and one instrument answering both is how they come to disagree.
1448
1563
  grades: Array.isArray(rs.grades) ? rs.grades : [],
1564
+ // the replan LEDGER the chain has spent — belted like the money and NOT like
1565
+ // the grades, because this one seeds a BOUND: a garbage or negative figure
1566
+ // that read through as a widening is the ceiling being refilled by a corrupt
1567
+ // spine (PRD v1.12). The latch rides beside the count, never derived from it.
1568
+ replans: typeof rs.replans === 'number' && Number.isFinite(rs.replans) && rs.replans > 0 ? Math.floor(rs.replans) : 0,
1569
+ replanGrantUsed: rs.replanGrantUsed === true,
1449
1570
  }
1450
1571
  : NO_PRIOR;
1451
1572
  const remainingCapUsd = envelope.perTryBudgetUsd - prior.spentUsd;
@@ -1459,7 +1580,10 @@ export async function runReuse(opts) {
1459
1580
  // `gradesInherited` is a COUNT, and only when there is one to state: the seam
1460
1581
  // carries stage names and numbers, never a close byte — the spine is append-only,
1461
1582
  // so a gap that crosses here crosses for good.
1462
- restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {}) } : null,
1583
+ restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {}),
1584
+ // WHERE it comes back to (v1.57 §3), stated before anything is spent rather than
1585
+ // discovered from the restarted leg's own `work-branch` record afterwards
1586
+ ...(rs.branch ? { branch: rs.branch } : {}) } : null,
1463
1587
  nextTry: rs ? null : startN,
1464
1588
  });
1465
1589
 
@@ -1497,7 +1621,7 @@ export async function runReuse(opts) {
1497
1621
  if (rs.mode === 'cold') {
1498
1622
  // every bridge try was already spent before the cold leg started: the loop below
1499
1623
  // has nothing left to authorize, and the restart IS the cold attempt
1500
- coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null };
1624
+ coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null, branch: rs.branch ?? null };
1501
1625
  startN = resolved.bridgeTries + 1;
1502
1626
  } else {
1503
1627
  const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === rs.bridge);
@@ -1513,7 +1637,7 @@ export async function runReuse(opts) {
1513
1637
  });
1514
1638
  }
1515
1639
  tried.add(entry.name);
1516
- const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null));
1640
+ const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null, rs.branch ?? null));
1517
1641
  if (stop) return stop;
1518
1642
  startN = rs.n + 1;
1519
1643
  }
@@ -1581,7 +1705,7 @@ export async function runReuse(opts) {
1581
1705
  // the verdict that attempt actually earned.
1582
1706
  const inheritedCold = coldRestart ? null : tries.find((t) => t.mode === 'cold' && t.inherited) ?? null;
1583
1707
  const cold = coldRestart
1584
- ? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed)
1708
+ ? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed, coldRestart.branch)
1585
1709
  : inheritedCold ?? await runTry(null, tries.length + 1);
1586
1710
  const coldStop = afterTry(cold);
1587
1711
  if (coldStop) return coldStop;
package/src/root.js CHANGED
@@ -30,6 +30,7 @@
30
30
  // UNRELIABLE red-set for comparison. Imported (never re-spelled) so the detector
31
31
  // keys off the exact string the shell emits — a magic string here would drift.
32
32
  import { GAP_KEEP_TRIM_MARKER } from './ralph.js';
33
+ import { GAP_TRIM_MARKER } from './kinds.js';
33
34
 
34
35
  /**
35
36
  * Strip a trailing duration stamp from a kept-failure line. Real `node --test`
@@ -107,7 +108,13 @@ export function createRoot({ gapKeep, redact = (s) => s, writesInformative = tru
107
108
  */
108
109
  const keptSet = (gap, pattern) => {
109
110
  const re = pattern ? new RegExp(pattern, 'm') : keepRe;
110
- if (!re || gap.includes(GAP_KEEP_TRIM_MARKER)) return null;
111
+ // BOTH trim announcements, because there are now two gap renderers: ralph's
112
+ // keep-block cap on a command close, and the kind executor's per-stage line
113
+ // cap on a DECLARED one. A trim the detector cannot see is worse than a
114
+ // missing detector — it makes the strong "reds unchanged" claim off a window
115
+ // that silently dropped failures (finding 5), which is the direction that
116
+ // manufactures a fixation reading out of an instrument that went blind.
117
+ if (!re || gap.includes(GAP_KEEP_TRIM_MARKER) || gap.includes(GAP_TRIM_MARKER)) return null;
111
118
  const lines = [...new Set(gap.split('\n').filter((l) => re.test(l)).map(normalizeRedLine))].sort();
112
119
  return lines.length ? lines : null;
113
120
  };
package/src/run.js CHANGED
@@ -116,6 +116,22 @@ async function primitiveSmoke(workdir) {
116
116
  * chain, not just the leg: without them a leg that re-grades an unchanged tree reports
117
117
  * `flat` — "revise the goal" — on a run that was converging when the money ran out.
118
118
  * Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
119
+ * @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
120
+ * replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
121
+ * `restart.replanGrantUsed`), forwarded to the plan flow, which seeds its ceiling with
122
+ * it. Its sibling above feeds a READOUT; this one feeds a BOUND, and that is the whole
123
+ * difference: `replanned`/`varianceGrantUsed` are locals in `runPlan`, so without this
124
+ * every kill hands the next leg a fresh allowance of an allowance PRD v1.12 makes the
125
+ * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
126
+ * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
127
+ * already sets, so a resume OF a resume folds once. Absent is the cold path.
128
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
129
+ * created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
130
+ * name is DERIVED from the signed spec and is therefore identical on every leg, so
131
+ * without this the restart would collide with its own predecessor's branch and the
132
+ * collision walk would mint a `-2` beside the work the resume exists to continue. The
133
+ * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
134
+ * verbatim to the plan flow; absent is the cold path.
119
135
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
120
136
  * territory, threaded to the plan flow. Defaults OFF
121
137
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -125,10 +141,10 @@ async function primitiveSmoke(workdir) {
125
141
  * binding.)
126
142
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
127
143
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
128
- * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
144
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
129
145
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
130
146
  */
131
- export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [] }) {
147
+ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null }) {
132
148
  // 0. the ledger's counters, declared FIRST so that every job-end — including
133
149
  // the pre-token reds below — can state a real figure. An omitted `spentUsd` is
134
150
  // not a zero: a consumer reads `undefined` and either crashes or launders it
@@ -204,6 +220,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
204
220
  // it inherited, and it is the only field that can carry it forward.
205
221
  ...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
206
222
  ...(typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? { priorWallMs } : {}),
223
+ // the REPLAN ledger, on the same declaration and for the same reason — a chain of
224
+ // resumes must fold once. This is the DIRECT-spine half of it: `readResume`'s
225
+ // `direct` mode opens its one implicit window on `job-start` and reads the fold off
226
+ // these very fields, so a U-path run resumed twice inherits leg 1's replans rather
227
+ // than restarting the ceiling at leg 2's window (PRD v1.12). Emitted only when there
228
+ // is one — a decorative `0` is indistinguishable from a run nobody folded, and the
229
+ // grant latch travels beside the count rather than being derived from it.
230
+ ...(typeof resumeReplans?.count === 'number' && Number.isFinite(resumeReplans.count) && resumeReplans.count > 0
231
+ ? { priorReplans: Math.floor(resumeReplans.count), priorReplanGrantUsed: resumeReplans.grantUsed === true }
232
+ : {}),
207
233
  });
208
234
 
209
235
  // 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
@@ -256,7 +282,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
256
282
  // accounts it natively (F12) and the job-end money contract is unchanged.
257
283
  {
258
284
  const outcome = await runPlan(job, {
259
- workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades,
285
+ workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
260
286
  remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
261
287
  isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
262
288
  spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
package/src/text.js CHANGED
@@ -4,6 +4,11 @@
4
4
  // are two instruments (F2/F21, the adaptlearn instrument caveat). It replaced
5
5
  // N0's stripFences when the fence-robust upgrade landed at N2. priceOf is the
6
6
  // ONE spelling of the honest-null cost read for the same reason (F6).
7
+ // `scrubRaw` is the third: the ONE way a model's raw output becomes a persisted
8
+ // record, for the same reason again — a second writer that forgets the scrub is
9
+ // a leak on the very trail the scrub exists to protect.
10
+
11
+ import { redactSecrets } from './validate.js';
7
12
 
8
13
  // A fence counts as the artifact's WRAPPER only when it opens within this many
9
14
  // lines of the response — the chatty-preamble shape ("Here's the fix:\n```…").
@@ -64,3 +69,118 @@ export function priceOf(r) {
64
69
  unpricedRounds: r?.metrics?.unpricedRounds ?? 0,
65
70
  };
66
71
  }
72
+
73
+ // ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
74
+ //
75
+ // Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
76
+ // copy of what the model actually said: the red named the position, the process
77
+ // exited, and the only artifact that could have decided whether the malformation
78
+ // is recurring (and therefore whether the scout must move to a structure-forced
79
+ // tool call) went with it. A raw that is not written down is a diagnosis nobody
80
+ // can make twice.
81
+ //
82
+ // Everything a persisted raw needs is HERE rather than at each writer, and the
83
+ // reason is the `scrubRed` reason one layer up: the scrub, the bound and the
84
+ // announcement are properties of PERSISTING A RAW, not of any one caller
85
+ // remembering them. A future writer that reaches for a raw reaches for this.
86
+
87
+ /**
88
+ * The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
89
+ * cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
90
+ * repository produces is a survey, the archive's real surveys ran 5991-8056
91
+ * bytes, and a record that cannot hold one whole specimen cannot answer the
92
+ * question it exists for. The two constants are deliberately independent
93
+ * numbers — one bounds a PROMPT, this one bounds a RECORD.
94
+ */
95
+ export const RAW_PERSIST_MAX = 8000;
96
+
97
+ /**
98
+ * The announced-trim marker for a persisted raw. A NEW spelling rather than a
99
+ * reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
100
+ * decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
101
+ * so Layer R must not compare it*, so a second producer of those bytes in a
102
+ * different population is exactly the F90 hazard — a reader must know every
103
+ * producer of its marker. Nothing reads this one yet; it exists so the trim
104
+ * announces itself (F28) and so a future reader has an unambiguous key.
105
+ */
106
+ export const RAW_TRIM_MARKER = 'raw trimmed:';
107
+
108
+ /**
109
+ * ONE raw model emission, ready to persist.
110
+ *
111
+ * Three things happen here and NONE of them is optional at a call site:
112
+ * - the text goes through the ONE redactor (`redactSecrets`, over the ONE
113
+ * `SECRET_PATTERNS` inventory). A raw is the single most likely record in
114
+ * this system to carry a live credential: it is whatever a model read out
115
+ * of a repository, verbatim, and it lands in a file that outlives the run;
116
+ * - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
117
+ * a bound eats detail, never evidence). The cut is decoded rather than
118
+ * sliced so a multi-byte character is never split in half;
119
+ * - it carries its own address: which call produced it (`label`, the same
120
+ * spelling the cost book meters under) and which attempt (`attempt`), plus
121
+ * the typed failure that describes it when there is one.
122
+ *
123
+ * `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
124
+ * absent, never an empty string that reads like a diagnosis nobody wrote.
125
+ * @param {{label: string, attempt: number, text: unknown, cause?: string|null,
126
+ * reason?: string|null, cap?: number}} o
127
+ * @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
128
+ * text: string, cause: string|null, reason: string|null}}
129
+ */
130
+ export function scrubRaw({ label, attempt, text, cause = null, reason = null, cap = RAW_PERSIST_MAX }) {
131
+ const scrubbed = redactSecrets(String(text ?? ''));
132
+ const buf = Buffer.from(scrubbed, 'utf8');
133
+ const trimmed = buf.length > cap;
134
+ // walk the cut BACK off any continuation byte (0b10xxxxxx): a byte-wise slice
135
+ // through a multi-byte character renders as a replacement char, and a record
136
+ // that garbles the evidence is a worse instrument than one that keeps a byte
137
+ // less of it
138
+ let end = cap;
139
+ while (trimmed && end > 0 && (buf[end] & 0xC0) === 0x80) end -= 1;
140
+ const kept = trimmed ? buf.subarray(0, end).toString('utf8') : scrubbed;
141
+ // the two diagnosis fields are rendered by `stampRaw` below, never here: the
142
+ // writer that stamps them LATER must go through the same one place, or the
143
+ // scrub becomes a property of when you knew the verdict
144
+ return stampRaw({
145
+ label,
146
+ attempt,
147
+ // the FULL size, always — the number a trim is measured against
148
+ bytes: buf.length,
149
+ trimmed,
150
+ // WITHHELD is measured off `end`, not off `cap`: the multi-byte walk-back above
151
+ // can cut up to three bytes short of the cap, and announcing `length - cap` would
152
+ // under-report the withheld bytes by exactly that much. An announced bound that
153
+ // states a number it did not withhold is the announcement failing at its one job.
154
+ text: trimmed
155
+ ? `${kept}\n[${RAW_TRIM_MARKER} ${buf.length - end} of ${buf.length} bytes withheld — the cap is ${cap} per raw]`
156
+ : kept,
157
+ cause: null,
158
+ reason: null,
159
+ }, { cause, reason });
160
+ }
161
+
162
+ /**
163
+ * Stamp a persisted raw with the verdict that DESCRIBES it — through the same
164
+ * boundary the raw's own text rode.
165
+ *
166
+ * It exists because the verdict is not known when the raw is created: a writer
167
+ * records the emission the moment the call returns (so a metered call can never
168
+ * be missing its output), and only classifies it afterwards. The obvious
169
+ * shape — assigning `cause`/`reason` onto the stored object at that later
170
+ * point — writes an UNSCRUBBED string into a record that is already past the
171
+ * one redactor, and `reason` is the worst field to do it to: it quotes the
172
+ * transport's own error prose, which nobody in this repo wrote and nobody
173
+ * bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
174
+ * its own two fields through it: ONE place decides how a diagnosis becomes
175
+ * persistable, exactly as ONE place decides it for the text.
176
+ *
177
+ * `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
178
+ * over a closed set would only hide a bug in the set.
179
+ * @template {{cause: string|null, reason: string|null}} T
180
+ * @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
181
+ */
182
+ export function stampRaw(raw, { cause = null, reason = null }) {
183
+ raw.cause = cause ?? null;
184
+ raw.reason = reason === null || reason === undefined ? null : redactSecrets(reason);
185
+ return raw;
186
+ }