bareloop 0.8.0 → 0.10.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
  }
@@ -575,6 +590,11 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
575
590
  const DECISIONS = {
576
591
  'cap-halt': [`Budget gate tripped mid-run (${e && e.message}). Continue with a higher cap, change approach, or stop?`,
577
592
  ['raise the cap and rerun', 'change the middle/harness', 'abandon the task']],
593
+ // No site in this tree mints `gate-red` any more: a BA-11 denial streak is
594
+ // a bounded attempt now (planrun's two `ask`s), not a terminal, because the
595
+ // fence HOLDING is governance working, not a fault. The entry stays because
596
+ // this catch is a dumb passthrough — it files whatever category the thrower
597
+ // names, so an unlisted one would read out with the generic text.
578
598
  'gate-red': ['The gate denied an action mid-run — the harness asked for something outside its scope.',
579
599
  ['widen the write scope deliberately', 'change the middle/harness', 'abandon the task']],
580
600
  'provider-red': ['The provider path failed mid-run (transport, not logic) — no verdict exists and the spend for the failed call is unknown (F6).',
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
@@ -641,7 +665,7 @@ function readGradeSeed(seen) {
641
665
  * which is later and better evidence than the last event). Defaults to the last
642
666
  * event's own timestamp — the last sign of life there is.
643
667
  * @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
644
- * perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
668
+ * perTryBudgetUsd, perTryWallMs, ended, endOutcome, wallDerivedHalt, greened, completed, tried,
645
669
  * selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
646
670
  * spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
647
671
  */
@@ -792,7 +816,50 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
792
816
  * CHECKPOINT and re-entering it is the ruling, not a second payment for an answer
793
817
  * already in hand. Empty by default, which is what keeps the reuse loop's own
794
818
  * graded-row semantics exactly where they were. */
795
- const haltedResumable = open?.jobEnd ? resumableHalt.has(String(open.jobEnd.outcome)) : false;
819
+ const recordedOutcome = open?.jobEnd ? String(open.jobEnd.outcome) : null;
820
+ /** D4a — the MISLABELLED wall stop, derived from the run's own primary records.
821
+ *
822
+ * `step-red` is a verdict-shaped terminal and stays non-resumable: a red is an
823
+ * answer. But on u-msmt91t3 the runner minted one 9.9 seconds AFTER its own
824
+ * `wall-bounded` record — a governance stop wearing a capability label, because the
825
+ * step-variance terminal did not re-read the clock (fixed at the source, src/planrun.js).
826
+ * Spines already written under the defect are not re-runnable and cannot be edited (a
827
+ * spine is append-only forever), so the READER derives the truth the file already
828
+ * holds: a run whose own record says the wall was crossed BEFORE it ended is a wall
829
+ * stop, and W-2's ruling applies — it keeps the grades it minted and pauses.
830
+ *
831
+ * Three constraints, each of them load-bearing:
832
+ * * a DERIVATION, never an override. The evidence is the run's own `wall-bounded`
833
+ * event, emitted by the runner at the round seam; no caller can assert this.
834
+ * * `step-red` ONLY, and only when `wall-halt` is itself resumable for this caller.
835
+ * Every other outcome — green, escalated, provider-red, a step-red with no wall
836
+ * record — is untouched, so "a red is an answer" survives intact.
837
+ * * it touches NO close verdict. `step-red` here is a GOVERNANCE stop label; the
838
+ * grades the close rendered are exactly what they were, and the resume seeds
839
+ * from them.
840
+ * @param {any[]} seen @param {any} jobEnd */
841
+ const wallCrossedBefore = (seen, jobEnd) => {
842
+ // strictly BEFORE the terminal: a record written after the run ended could not have
843
+ // been what stopped it. `indexOf` on the window's own array is the ordering there is
844
+ // (the reader never re-sorts), and a jobEnd somehow absent from it reads nothing.
845
+ const end = seen.indexOf(jobEnd);
846
+ if (end < 0) return false;
847
+ for (let i = 0; i < end; i += 1) {
848
+ const e = seen[i];
849
+ if (!isObj(e) || e.type !== 'wall-bounded' || e.bounded !== true) continue;
850
+ // the clock's OWN two spellings of "the deadline passed", both off `clock.report`.
851
+ // Never a re-derivation from timestamps: a second arithmetic for one question is
852
+ // how two instruments come to disagree.
853
+ if (e.remainingMs === 0) return true;
854
+ if (typeof e.elapsedMs === 'number' && typeof e.requestedMs === 'number' && e.elapsedMs >= e.requestedMs) return true;
855
+ }
856
+ return false;
857
+ };
858
+ const wallDerivedHalt = recordedOutcome === 'step-red'
859
+ && resumableHalt.has('wall-halt')
860
+ && wallCrossedBefore(open.seen, open.jobEnd);
861
+ const haltedResumable = recordedOutcome !== null
862
+ && (resumableHalt.has(recordedOutcome) || wallDerivedHalt);
796
863
  if (open) {
797
864
  if (open.jobEnd && !haltedResumable) {
798
865
  // GRADED: runJob returned and the close had its say. Not a restart — the row is
@@ -842,6 +909,17 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
842
909
  // restart already re-graded that tree. Counting both would enter the same
843
910
  // reading twice and flatten a chain that moved.
844
911
  grades: readGradeSeed(open.seen),
912
+ // the WORK BRANCH this leg's uncommitted work is sitting on (v1.57 §3). Read
913
+ // from THIS window first and then from the newest abandoned window of the same
914
+ // try, on the checkpoint's own rule and for the checkpoint's own reason: the
915
+ // branch — like the plan — is a fact about the TREE, and the tree survives an
916
+ // attempt that died before recording anything of its own. Null is the honest
917
+ // answer for a leg killed before 0c (or a spine older than the record); the
918
+ // resume then takes the cold path and creates one, which is correct, because a
919
+ // leg that never branched has no work to strand.
920
+ branch: readWorkBranch(open.seen)
921
+ ?? abandoned.filter((w) => w.n === open.n).map((w) => readWorkBranch(w.seen)).filter(Boolean).at(-1)
922
+ ?? null,
845
923
  // the REPLAN ledger, folded like the money one line below and deliberately
846
924
  // NOT like the grades one line above. A grade seed feeds a READOUT and its
847
925
  // documented shortening chain can only under-claim a direction; this feeds a
@@ -928,7 +1006,14 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
928
1006
  perTryBudgetUsd: head?.perTryBudgetUsd ?? directHead?.budgetUsd ?? null,
929
1007
  perTryWallMs: head?.perTryWallMs ?? null,
930
1008
  ended: end !== null || (directEnd !== null && !haltedResumable),
1009
+ // the RECORDED terminal, always — the derivation below never rewrites what the run
1010
+ // said about itself, it only says how this reader read it
931
1011
  endOutcome: end?.outcome ?? directEnd?.outcome ?? null,
1012
+ /** D4a — true when a recorded `step-red` was re-read as a wall-halt off this run's
1013
+ * OWN `wall-bounded` record. Surfaced rather than silent: an operator resuming a
1014
+ * run whose spine says `step-red` must be told which record turned it into a
1015
+ * checkpoint, or the gate looks like it simply stopped refusing. */
1016
+ wallDerivedHalt,
932
1017
  greened: completed.some((t) => t.verdictClass === 'green'),
933
1018
  completed,
934
1019
  tried,
@@ -1170,8 +1255,14 @@ export async function runReuse(opts) {
1170
1255
  * much of the try's allowance is gone; this says how much of its WORK is done, so the
1171
1256
  * restart re-pays for neither the scout, the draft, nor a finished step. Null is the
1172
1257
  * scout/draft death, where nothing paid is re-payable and the try simply restarts.
1258
+ * @param {string|null} [branch] RESUME (v1.57 §3) — the WORK BRANCH that attempt's work
1259
+ * is sitting on (`readResume`'s `restart.branch`). The name is derived from the SIGNED
1260
+ * spec and is therefore the same on every leg, so a restart that recomputed it would
1261
+ * collide with its own predecessor and be handed a fresh `-2` beside the work it came
1262
+ * back for. Declared and inherited, exactly like the money and the replan ledger. Null
1263
+ * is the cold path, and also the leg killed before it ever branched.
1173
1264
  * @returns {Promise<any>} the try row */
1174
- const runTry = async (bridge, n, prior = NO_PRIOR, seed = null) => {
1265
+ const runTry = async (bridge, n, prior = NO_PRIOR, seed = null, branch = null) => {
1175
1266
  const mode = bridge ? 'bridge' : 'cold';
1176
1267
  const name = bridge ? bridge.name : null;
1177
1268
  const resumed = prior.spentUsd > 0 || prior.wallMs > 0;
@@ -1231,6 +1322,10 @@ export async function runReuse(opts) {
1231
1322
  // a bound the operator signed, widened by a kill (PRD v1.12).
1232
1323
  ...(prior.replans > 0 ? { resumeReplans: { count: prior.replans, grantUsed: prior.replanGrantUsed } } : {}),
1233
1324
  ...(seed ? { resumeSeed: seed } : {}),
1325
+ // …and WHERE its work is. Same omission class as the three folds above, with its
1326
+ // own cost: a branch that never arrives is not a wrong number but a SECOND branch,
1327
+ // and the leg would carry on beside the tree it was resumed to continue.
1328
+ ...(branch ? { resumeBranch: branch } : {}),
1234
1329
  bridge,
1235
1330
  });
1236
1331
  } catch (e) {
@@ -1290,7 +1385,7 @@ export async function runReuse(opts) {
1290
1385
  name: name ?? job.job,
1291
1386
  goal: job.goal,
1292
1387
  specHash: trySpecHash,
1293
- closeStageNames: (stageClose(job.close) ?? []).map((/** @type {any} */ s) => s.name),
1388
+ closeStageNames: (closeStagesOf(job) ?? []).map((/** @type {any} */ s) => s.name),
1294
1389
  toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
1295
1390
  };
1296
1391
  row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
@@ -1423,6 +1518,19 @@ export async function runReuse(opts) {
1423
1518
  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.',
1424
1519
  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'],
1425
1520
  },
1521
+ 'branch-red': {
1522
+ // NOT re-filed, for `interpreter-red`'s reason plus one of its own: the runner
1523
+ // already escalated it at the fault's own site, and the fault is the OPERATOR's
1524
+ // state (a patient that is not a git checkout, a crowded branch namespace),
1525
+ // never a library failing — the ledger excludes it for exactly that.
1526
+ refile: false,
1527
+ 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.',
1528
+ options: [
1529
+ 'make the patient a git checkout with at least one commit',
1530
+ 'free the work-branch name (delete or rename the stale branches this run collided with)',
1531
+ 'abandon the run',
1532
+ ],
1533
+ },
1426
1534
  'interpreter-red': {
1427
1535
  // NOT re-filed as an escalation. `interpreter-red` is one of the categories the
1428
1536
  // ledger CLASSIFIES — it aims a runtime-red at a library — and runJob already
@@ -1522,7 +1630,10 @@ export async function runReuse(opts) {
1522
1630
  // `gradesInherited` is a COUNT, and only when there is one to state: the seam
1523
1631
  // carries stage names and numbers, never a close byte — the spine is append-only,
1524
1632
  // so a gap that crosses here crosses for good.
1525
- 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,
1633
+ 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 } : {}),
1634
+ // WHERE it comes back to (v1.57 §3), stated before anything is spent rather than
1635
+ // discovered from the restarted leg's own `work-branch` record afterwards
1636
+ ...(rs.branch ? { branch: rs.branch } : {}) } : null,
1526
1637
  nextTry: rs ? null : startN,
1527
1638
  });
1528
1639
 
@@ -1560,7 +1671,7 @@ export async function runReuse(opts) {
1560
1671
  if (rs.mode === 'cold') {
1561
1672
  // every bridge try was already spent before the cold leg started: the loop below
1562
1673
  // has nothing left to authorize, and the restart IS the cold attempt
1563
- coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null };
1674
+ coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null, branch: rs.branch ?? null };
1564
1675
  startN = resolved.bridgeTries + 1;
1565
1676
  } else {
1566
1677
  const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === rs.bridge);
@@ -1576,7 +1687,7 @@ export async function runReuse(opts) {
1576
1687
  });
1577
1688
  }
1578
1689
  tried.add(entry.name);
1579
- const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null));
1690
+ const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null, rs.branch ?? null));
1580
1691
  if (stop) return stop;
1581
1692
  startN = rs.n + 1;
1582
1693
  }
@@ -1644,7 +1755,7 @@ export async function runReuse(opts) {
1644
1755
  // the verdict that attempt actually earned.
1645
1756
  const inheritedCold = coldRestart ? null : tries.find((t) => t.mode === 'cold' && t.inherited) ?? null;
1646
1757
  const cold = coldRestart
1647
- ? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed)
1758
+ ? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed, coldRestart.branch)
1648
1759
  : inheritedCold ?? await runTry(null, tries.length + 1);
1649
1760
  const coldStop = afterTry(cold);
1650
1761
  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
@@ -15,6 +15,7 @@ import { join } from 'node:path';
15
15
  import { LiteCtx } from 'litectx';
16
16
  import { validateJob, jobSpecHash, checkApproval } from './job.js';
17
17
  import { runPlan } from './planrun.js';
18
+ import { SMOKE_STORE } from './kinds.js';
18
19
 
19
20
  /** @typedef {{code: string, path: string, detail?: string}} Red */
20
21
 
@@ -29,7 +30,7 @@ import { runPlan } from './planrun.js';
29
30
  */
30
31
  async function primitiveSmoke(workdir) {
31
32
  try {
32
- const lc = new LiteCtx({ root: join(workdir, '.smoke') });
33
+ const lc = new LiteCtx({ root: join(workdir, SMOKE_STORE) });
33
34
  await lc.remember('smoke-known-answer', 'bareloop primitive smoke: the answer is 42', { kind: 'fact' });
34
35
  const hits = await lc.recall('bareloop primitive smoke known answer', { kind: 'fact', n: 5, body: true });
35
36
  const ok = hits.some((h) => String(h.body ?? '').includes('the answer is 42'));
@@ -125,6 +126,13 @@ async function primitiveSmoke(workdir) {
125
126
  * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
126
127
  * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
127
128
  * already sets, so a resume OF a resume folds once. Absent is the cold path.
129
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
130
+ * created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
131
+ * name is DERIVED from the signed spec and is therefore identical on every leg, so
132
+ * without this the restart would collide with its own predecessor's branch and the
133
+ * collision walk would mint a `-2` beside the work the resume exists to continue. The
134
+ * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
135
+ * verbatim to the plan flow; absent is the cold path.
128
136
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
129
137
  * territory, threaded to the plan flow. Defaults OFF
130
138
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -134,10 +142,10 @@ async function primitiveSmoke(workdir) {
134
142
  * binding.)
135
143
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
136
144
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
137
- * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
145
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
138
146
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
139
147
  */
140
- 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 }) {
148
+ 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 }) {
141
149
  // 0. the ledger's counters, declared FIRST so that every job-end — including
142
150
  // the pre-token reds below — can state a real figure. An omitted `spentUsd` is
143
151
  // not a zero: a consumer reads `undefined` and either crashes or launders it
@@ -275,7 +283,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
275
283
  // accounts it natively (F12) and the job-end money contract is unchanged.
276
284
  {
277
285
  const outcome = await runPlan(job, {
278
- workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans,
286
+ workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
279
287
  remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
280
288
  isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
281
289
  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,197 @@ export function priceOf(r) {
64
69
  unpricedRounds: r?.metrics?.unpricedRounds ?? 0,
65
70
  };
66
71
  }
72
+
73
+ /**
74
+ * The F6 tally over a list of PRICED CALLS — `priceOf`'s per-call read, summed.
75
+ *
76
+ * `knownUsd` carries the priced half explicitly and `costUsd` is the honest
77
+ * total: null the moment anything went unpriced, never the priced half wearing a
78
+ * complete coat. An EMPTY list is a complete $0 and not an unknown — nothing
79
+ * spent is a number somebody can act on.
80
+ * @param {{costUsd: number|null, unpricedRounds?: number}[]|null|undefined} entries
81
+ * @returns {{costUsd: number|null, knownUsd: number, spendComplete: boolean,
82
+ * nullCostCalls: number, unpricedRounds: number}}
83
+ */
84
+ export function tallyCalls(entries) {
85
+ const list = Array.isArray(entries) ? entries : [];
86
+ const known = list.reduce((a, e) => a + (typeof e.costUsd === 'number' ? e.costUsd : 0), 0);
87
+ const nullCostCalls = list.filter((e) => e.costUsd === null).length;
88
+ const unpricedRounds = list.reduce((a, e) => a + (e.unpricedRounds || 0), 0);
89
+ const spendComplete = nullCostCalls === 0 && unpricedRounds === 0;
90
+ return {
91
+ costUsd: spendComplete ? Number(known.toFixed(6)) : null,
92
+ knownUsd: Number(known.toFixed(6)),
93
+ spendComplete,
94
+ nullCostCalls,
95
+ unpricedRounds,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * THE ONE MONEY-CEILING PREDICATE, for the same reason `priceOf` above is one:
101
+ * the authoring pipeline meters two populations of paid call in two different
102
+ * accumulators (the survey's `calls[]`, the declaration loop's cost book), and
103
+ * two hand-spelled answers to "am I over the ceiling?" is two instruments.
104
+ *
105
+ * It is asked BETWEEN metered calls and never inside one. A cap that binds
106
+ * mid-call kills the row before it can be graded — F45 measured that as 3 of 7
107
+ * launches lost and $5.82 unreadable.
108
+ *
109
+ * Three answers, and the order between the last two is load-bearing:
110
+ * - NO CEILING → null, always. An absent cap is UNBOUNDED, and unbounded is a
111
+ * visible operator choice, never a defaulted number nobody set (the
112
+ * `maxWallMs` precedent). There is nothing to enforce, so unpriced spend
113
+ * here is merely reported.
114
+ * - KNOWN SPEND AT OR OVER THE CEILING → `cap-halt`, even when the total is
115
+ * unknown. The breach is CERTAIN on the priced half alone — whatever the
116
+ * unpriced remainder turns out to be, it can only be larger — so naming this
117
+ * `pricing-red` would send the operator to fix a meter when the fact is that
118
+ * the money is gone. At-or-over rather than over: the next call is the one
119
+ * that breaches, and it is the one being decided.
120
+ * - SPEND THAT CANNOT BE KNOWN → `pricing-red`. `?? 0` launders unknown into
121
+ * $0 (F6) and a ceiling enforced against a laundered floor is not enforced
122
+ * at all. If the spend cannot be seen the cap cannot govern it, and that is
123
+ * its own stop rather than a silent pass.
124
+ *
125
+ * A MALFORMED ceiling is the one input it refuses rather than answers: `null` and
126
+ * an absent field mean UNBOUNDED, and they are the ONLY way to get there. Anything
127
+ * else that is not a finite number (`'2.50'`, `NaN`, `Infinity`) reading as
128
+ * unbounded would be F6's laundering in its ceiling form — a caller who set a cap
129
+ * getting a paid pipeline with nothing enforcing it, which is the exact collapse
130
+ * the ceiling exists to prevent, wearing the caller's own typo (PRD v1.62). The
131
+ * CLI's `parseCeiling` guards the FLAG; this guards the LIBRARY, and `authorClose`
132
+ * / `runAuthorScout` / `authorCloseForJob` are documented adopter seams. It THROWS
133
+ * rather than clamping, unlike the attempt caps beside it, because those axes have
134
+ * a safe direction to clamp toward and this one does not: every candidate default
135
+ * is either a number nobody set or the unbounded run the caller just declined.
136
+ * @param {{ceilingUsd: number|null|undefined, knownUsd: number, spendComplete: boolean}} o
137
+ * @returns {'cap-halt'|'pricing-red'|null}
138
+ * @throws {Error} when the ceiling is neither absent nor a finite number
139
+ */
140
+ export function capStop({ ceilingUsd, knownUsd, spendComplete }) {
141
+ if (ceilingUsd === null || ceilingUsd === undefined) return null;
142
+ if (typeof ceilingUsd !== 'number' || !Number.isFinite(ceilingUsd)) {
143
+ throw new Error(`capStop: a money ceiling must be a finite number of dollars or null — got ${typeof ceilingUsd} `
144
+ + `${String(ceilingUsd)}. A malformed ceiling is an ERROR, never a silent fall back to UNBOUNDED: omit it, or `
145
+ + 'pass an explicit null, to run unbounded — that is a stated operator choice and never a typo arrived at');
146
+ }
147
+ if (knownUsd >= ceilingUsd) return 'cap-halt';
148
+ if (!spendComplete) return 'pricing-red';
149
+ return null;
150
+ }
151
+
152
+ // ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
153
+ //
154
+ // Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
155
+ // copy of what the model actually said: the red named the position, the process
156
+ // exited, and the only artifact that could have decided whether the malformation
157
+ // is recurring (and therefore whether the scout must move to a structure-forced
158
+ // tool call) went with it. A raw that is not written down is a diagnosis nobody
159
+ // can make twice.
160
+ //
161
+ // Everything a persisted raw needs is HERE rather than at each writer, and the
162
+ // reason is the `scrubRed` reason one layer up: the scrub, the bound and the
163
+ // announcement are properties of PERSISTING A RAW, not of any one caller
164
+ // remembering them. A future writer that reaches for a raw reaches for this.
165
+
166
+ /**
167
+ * The ceiling on ONE persisted raw. Sized to the survey's own prompt-ingredient
168
+ * cap (`AUTHOR_SCOUT_BLOB_MAX`, 8000) rather than picked: the largest raw this
169
+ * repository produces is a survey, the archive's real surveys ran 5991-8056
170
+ * bytes, and a record that cannot hold one whole specimen cannot answer the
171
+ * question it exists for. The two constants are deliberately independent
172
+ * numbers — one bounds a PROMPT, this one bounds a RECORD.
173
+ */
174
+ export const RAW_PERSIST_MAX = 8000;
175
+
176
+ /**
177
+ * The announced-trim marker for a persisted raw. A NEW spelling rather than a
178
+ * reuse of `GAP_TRIM_MARKER` ('gap trimmed:'), and that is a decision, not
179
+ * decoration: `src/root.js` READS `GAP_TRIM_MARKER` to mean *this gap was cut,
180
+ * so Layer R must not compare it*, so a second producer of those bytes in a
181
+ * different population is exactly the F90 hazard — a reader must know every
182
+ * producer of its marker. Nothing reads this one yet; it exists so the trim
183
+ * announces itself (F28) and so a future reader has an unambiguous key.
184
+ */
185
+ export const RAW_TRIM_MARKER = 'raw trimmed:';
186
+
187
+ /**
188
+ * ONE raw model emission, ready to persist.
189
+ *
190
+ * Three things happen here and NONE of them is optional at a call site:
191
+ * - the text goes through the ONE redactor (`redactSecrets`, over the ONE
192
+ * `SECRET_PATTERNS` inventory). A raw is the single most likely record in
193
+ * this system to carry a live credential: it is whatever a model read out
194
+ * of a repository, verbatim, and it lands in a file that outlives the run;
195
+ * - it is BOUNDED, and the bound ANNOUNCES itself with the full size (F28 —
196
+ * a bound eats detail, never evidence). The cut is decoded rather than
197
+ * sliced so a multi-byte character is never split in half;
198
+ * - it carries its own address: which call produced it (`label`, the same
199
+ * spelling the cost book meters under) and which attempt (`attempt`), plus
200
+ * the typed failure that describes it when there is one.
201
+ *
202
+ * `cause`/`reason` are `null` when the emission was fine — an ABSENT failure is
203
+ * absent, never an empty string that reads like a diagnosis nobody wrote.
204
+ * @param {{label: string, attempt: number, text: unknown, cause?: string|null,
205
+ * reason?: string|null, cap?: number}} o
206
+ * @returns {{label: string, attempt: number, bytes: number, trimmed: boolean,
207
+ * text: string, cause: string|null, reason: string|null}}
208
+ */
209
+ export function scrubRaw({ label, attempt, text, cause = null, reason = null, cap = RAW_PERSIST_MAX }) {
210
+ const scrubbed = redactSecrets(String(text ?? ''));
211
+ const buf = Buffer.from(scrubbed, 'utf8');
212
+ const trimmed = buf.length > cap;
213
+ // walk the cut BACK off any continuation byte (0b10xxxxxx): a byte-wise slice
214
+ // through a multi-byte character renders as a replacement char, and a record
215
+ // that garbles the evidence is a worse instrument than one that keeps a byte
216
+ // less of it
217
+ let end = cap;
218
+ while (trimmed && end > 0 && (buf[end] & 0xC0) === 0x80) end -= 1;
219
+ const kept = trimmed ? buf.subarray(0, end).toString('utf8') : scrubbed;
220
+ // the two diagnosis fields are rendered by `stampRaw` below, never here: the
221
+ // writer that stamps them LATER must go through the same one place, or the
222
+ // scrub becomes a property of when you knew the verdict
223
+ return stampRaw({
224
+ label,
225
+ attempt,
226
+ // the FULL size, always — the number a trim is measured against
227
+ bytes: buf.length,
228
+ trimmed,
229
+ // WITHHELD is measured off `end`, not off `cap`: the multi-byte walk-back above
230
+ // can cut up to three bytes short of the cap, and announcing `length - cap` would
231
+ // under-report the withheld bytes by exactly that much. An announced bound that
232
+ // states a number it did not withhold is the announcement failing at its one job.
233
+ text: trimmed
234
+ ? `${kept}\n[${RAW_TRIM_MARKER} ${buf.length - end} of ${buf.length} bytes withheld — the cap is ${cap} per raw]`
235
+ : kept,
236
+ cause: null,
237
+ reason: null,
238
+ }, { cause, reason });
239
+ }
240
+
241
+ /**
242
+ * Stamp a persisted raw with the verdict that DESCRIBES it — through the same
243
+ * boundary the raw's own text rode.
244
+ *
245
+ * It exists because the verdict is not known when the raw is created: a writer
246
+ * records the emission the moment the call returns (so a metered call can never
247
+ * be missing its output), and only classifies it afterwards. The obvious
248
+ * shape — assigning `cause`/`reason` onto the stored object at that later
249
+ * point — writes an UNSCRUBBED string into a record that is already past the
250
+ * one redactor, and `reason` is the worst field to do it to: it quotes the
251
+ * transport's own error prose, which nobody in this repo wrote and nobody
252
+ * bounds. So the stamp is a function, not an assignment, and `scrubRaw` renders
253
+ * its own two fields through it: ONE place decides how a diagnosis becomes
254
+ * persistable, exactly as ONE place decides it for the text.
255
+ *
256
+ * `cause` is enumerated (`SURVEY_CAUSES`) and passes through untouched; a mask
257
+ * over a closed set would only hide a bug in the set.
258
+ * @template {{cause: string|null, reason: string|null}} T
259
+ * @param {T} raw @param {{cause?: string|null, reason?: string|null}} o @returns {T}
260
+ */
261
+ export function stampRaw(raw, { cause = null, reason = null }) {
262
+ raw.cause = cause ?? null;
263
+ raw.reason = reason === null || reason === undefined ? null : redactSecrets(reason);
264
+ return raw;
265
+ }
package/src/tools.js CHANGED
@@ -31,6 +31,7 @@ import { extractArtifact, priceOf } from './text.js';
31
31
  // caller can forget to pass, and this leak has to be inexpressible, not optional.
32
32
  // Same ONE inventory the spine channel and the close output ride through.
33
33
  import { redactSecrets } from './validate.js';
34
+ import { GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
34
35
 
35
36
  /** @typedef {{body?: string|null, text?: string|null}} RecallHit litectx recall hit — body present only with `{body: true}` */
36
37
 
@@ -45,6 +46,16 @@ const PERSONA = 'You are a senior engineer. Reply with ONLY the complete content
45
46
  // cheap, wrong attempt the rational move — which is exactly what the loop wants.
46
47
  export const PERSONA_TOOLS = 'You are a senior engineer working in a repository through file tools. '
47
48
  + 'ALWAYS use absolute paths — relative paths resolve against the process, not the repository, and will be denied. '
49
+ // F98's residual: the fence ALREADY denies these (planrun's Gate deny list), and the
50
+ // spine is written outside the patient entirely — but nothing ever SAID so, and a
51
+ // denial is a wall while a stated law is a map. The way a worker learned the rule was
52
+ // by spending rounds of a bounded attempt probing bookkeeping files, and the rounds a
53
+ // probe costs are rounds the fix does not get. Registered here, beside the other fence
54
+ // fact a worker cannot infer, so it renders for every worker on every grant.
55
+ // the names are SPELLED FROM the one home (`ARBITER_BOOK_STORES`) so the stated
56
+ // law can never drift from the fence that enforces it
57
+ + `The repository root also holds the arbiter's own books — ${GATE_AUDIT_FILE}, ${ARBITER_BOOK_STORES.join(', ')}, and this run's spine. `
58
+ + 'They are ALWAYS denied and record how you are being judged, never anything about the task: never read them, and never try to route around the denial. '
48
59
  + 'You are ONE attempt inside an automated loop: when you finish, a test suite runs and, if it still fails, you are called again with its output. '
49
60
  + 'So do not try to be certain before acting. Read only what you need to form your best hypothesis, make the change with the write tool, and stop. '
50
61
  + 'A wrong cheap attempt is corrected by the next round; exhaustive reading is not — every file you read is re-sent on every later round and the run has a hard budget it can exhaust before you ever write. '