bareloop 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +283 -0
- package/README.md +4 -2
- package/bareloop.context.md +156 -18
- package/package.json +1 -1
- package/src/exits.js +8 -2
- package/src/job.js +43 -6
- package/src/ledger.js +57 -9
- package/src/plan.js +31 -100
- package/src/planrun.js +318 -62
- package/src/ralph.js +8 -1
- package/src/reuse.js +67 -4
- package/src/run.js +21 -2
- package/src/validate.js +121 -0
- package/types/exits.d.ts +14 -0
- package/types/job.d.ts +13 -2
- package/types/plan.d.ts +23 -38
- package/types/planrun.d.ts +116 -16
- package/types/ralph.d.ts +16 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +14 -1
- package/types/validate.d.ts +50 -0
package/src/planrun.js
CHANGED
|
@@ -19,10 +19,10 @@ import { createHash } from 'node:crypto';
|
|
|
19
19
|
import { join, resolve, relative } from 'node:path';
|
|
20
20
|
import { Gate } from 'bareguard';
|
|
21
21
|
import { LiteCtx } from 'litectx';
|
|
22
|
-
import { runClose, runStages, ralph, CLOSE_FAULTS } from './ralph.js';
|
|
22
|
+
import { runClose, runStages, ralph, CLOSE_FAULTS, boundGap } from './ralph.js';
|
|
23
23
|
import { validatePlan, legalScopes, stageClose } from './plan.js';
|
|
24
|
-
import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU
|
|
25
|
-
import { snapshotScope, evalExits } from './exits.js';
|
|
24
|
+
import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU } from './plan.js';
|
|
25
|
+
import { snapshotScope, evalExits, CHECK_GAP_MAX } from './exits.js';
|
|
26
26
|
import { createRoot } from './root.js';
|
|
27
27
|
import { createStallWatch, STALL_MS, MAX_STALLS } from './stall.js';
|
|
28
28
|
import { createLadder, STRIKE_LIMIT } from './ladder.js';
|
|
@@ -157,40 +157,104 @@ required above, and nothing else.
|
|
|
157
157
|
${JSON.stringify(plan, null, 2)}`;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/** The red-set for the replan brief's copy of a step's exit gap: every non-blank
|
|
161
|
+
* line. It is `\S` and not the close stage's own `gapKeep` for the reason Layer R
|
|
162
|
+
* already pays at the step seam (see `createRoot` below) — a shipped `gapKeep` is
|
|
163
|
+
* `^`-anchored (`^BAREAGENT `, `^red`, `^FAILED`) and the exit evaluator wraps the
|
|
164
|
+
* stage's output in `check "x" red: …`, so the anchor no longer sits where the
|
|
165
|
+
* pattern expects it. A gap that reaches here is ALREADY a red-set: `judge` builds
|
|
166
|
+
* it out of the failing exits only, so every line in it is a red line and keeping
|
|
167
|
+
* all of them is the correct instrument, not a widened one. */
|
|
168
|
+
const REPLAN_GAP_KEEP = '\\S';
|
|
169
|
+
|
|
160
170
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
171
|
+
* This step's own last EXIT results, rendered for the REPLAN brief.
|
|
172
|
+
*
|
|
173
|
+
* The label says exits and not "the close", and the distinction is the whole of one
|
|
174
|
+
* fix: `res.gap` is `lastGap`, the join of EVERY failing exit's detail in exit order.
|
|
175
|
+
* With `MAX_EXITS_PER_STEP` at 2 and the mandatory `tree-changed` pairing on a write
|
|
176
|
+
* step, a step that wrote nothing leads the block with the EVALUATOR's own prose
|
|
177
|
+
* (`0 files changed under src/** …`) — captured verbatim from a real replan prompt,
|
|
178
|
+
* under a label reading "the verification's own output". The close stage's output is
|
|
179
|
+
* one of the details here, never reliably all of them. The payload is left alone: "the
|
|
180
|
+
* step wrote nothing" is exactly what a replanner should be told, and stripping it to
|
|
181
|
+
* make an old label true would trade a real fact for a tidy sentence.
|
|
182
|
+
*
|
|
183
|
+
* u-mshcpdg4: a run one strict error from done, killed with $1.10 and 82 seconds
|
|
184
|
+
* unspent. The close named the remaining work exactly — `src/recurse.js(978,115)`
|
|
185
|
+
* — and the worker read that every attempt. The REPLANNER, which is the component
|
|
186
|
+
* that chooses which files the next plan targets, was handed only the trend line
|
|
187
|
+
* (`still progressing — typecheck 30 → 15 → 15 → 1`): converging, and silent about
|
|
188
|
+
* WHERE. It re-targeted `src/loop.js`, already at zero errors; that step wrote
|
|
189
|
+
* nothing and struck out. F85 gave the brief the trajectory; this gives it the
|
|
190
|
+
* artifact the trajectory is a summary OF.
|
|
191
|
+
*
|
|
192
|
+
* Handed over as TEXT, never as a parsed file list, and the trap is worth naming
|
|
193
|
+
* because the parse is the obvious-looking fix: line 1 of that very gap reads
|
|
194
|
+
* `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and
|
|
195
|
+
* only line 2 names the culprit. The `never wrote` advisory that used to sit here
|
|
196
|
+
* did exactly that parse, and on this gap it resolved to `src/loop.js` — the file
|
|
197
|
+
* already CLEAN — then said so as a directive beside the artifact that said the
|
|
198
|
+
* opposite. It was deleted with this change (hamr, 2026-08-05): two readers of one
|
|
199
|
+
* question, the parsed one wrong, is the same failure the variance meter just had.
|
|
200
|
+
* Nothing generic can tell a summary line from a detail line, and the
|
|
201
|
+
* shapes differ per close anyway (tsc file:line, pytest test ids, a count close
|
|
202
|
+
* that names no file at all). A model reading the artifact can make that
|
|
203
|
+
* distinction; a regex reproduces the bug. So: no extraction, no digest, no
|
|
204
|
+
* inference — the artifact, bounded and scrubbed.
|
|
205
|
+
*
|
|
206
|
+
* Two boundaries, both reused rather than respelled:
|
|
207
|
+
* - SCRUB. A prompt is an egress point and this repo has ONE secret inventory
|
|
208
|
+
* (`SECRET_PATTERNS` via `redactSecrets`). The gap arrives already scrubbed
|
|
209
|
+
* from `judge`, which makes this defense in depth rather than the only guard —
|
|
210
|
+
* and defense in depth at the egress is exactly where it belongs, because the
|
|
211
|
+
* next caller of this helper will not remember the upstream one.
|
|
212
|
+
* - BOUND, and it is a BACKSTOP rather than an envelope. What arrives here has
|
|
213
|
+
* ALREADY been bounded: `runClose` ran `boundGap` with the stage's own `gapKeep`,
|
|
214
|
+
* which deliberately rescues the `not ok`/`FAILED` names out of the elided middle
|
|
215
|
+
* — the mechanical gap F46's conversion mechanism feeds the worker. Re-bounding
|
|
216
|
+
* that with a fresh 400/1500 envelope deletes exactly those names a second time,
|
|
217
|
+
* which is F28 reintroduced; measured on a 120-red-line close output, the first
|
|
218
|
+
* bound rescued 78 names and this one dropped 12 of them. src/exits.js states the
|
|
219
|
+
* same rule for the same artifact one seam earlier, and this now uses ITS number:
|
|
220
|
+
* `CHECK_GAP_MAX`, imported rather than respelled, so the two seams cannot drift
|
|
221
|
+
* on how big a check gap may be. Under it the gap passes through VERBATIM.
|
|
222
|
+
*
|
|
223
|
+
* A backstop is still needed, and the headroom is why the ceiling is not smaller.
|
|
224
|
+
* `res.gap` is `lastGap`, the JOIN of up to `MAX_EXITS_PER_STEP` failing exit
|
|
225
|
+
* details, and the check-passes branch slices EACH at `CHECK_GAP_MAX` — so what
|
|
226
|
+
* arrives is bounded per detail, never in total. Measured, the real maximum is
|
|
227
|
+
* comfortably under: `runClose`'s own envelope tops out at ~10.1KB (400 head +
|
|
228
|
+
* 8192 keep + 1500 tail + labels), and joined with a `tree-changed` line that is
|
|
229
|
+
* ~10.2KB, so the shipped path passes through untouched with ~1.8KB to spare. The
|
|
230
|
+
* backstop therefore fires only where src/exits.js says it should: a seam that
|
|
231
|
+
* returned something never bounded at all.
|
|
172
232
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
233
|
+
* Over the line `boundGap` runs with `REPLAN_GAP_KEEP`, the same envelope the
|
|
234
|
+
* close path uses, and every trim announces itself (F28). Note what that keep-set
|
|
235
|
+
* cannot do: `\S` matches every non-blank line, so the keep block degenerates into
|
|
236
|
+
* "the first 50 lines" and re-surfaces nothing from the middle. That is the honest
|
|
237
|
+
* limit of a red-set that cannot know the stage's own pattern (see
|
|
238
|
+
* `REPLAN_GAP_KEEP`), and it is affordable precisely because it now applies only
|
|
239
|
+
* to a gap nothing upstream ever bounded — on that gap ANY envelope loses names,
|
|
240
|
+
* and the choice is a bounded prompt over an unbounded one.
|
|
241
|
+
*
|
|
242
|
+
* No gap → the empty string, so the brief renders byte-identically to the pre-F86
|
|
243
|
+
* one. A labelled empty section would be an invitation to explain an absence the
|
|
244
|
+
* run never observed (a stall never judged its exits at all).
|
|
245
|
+
*
|
|
246
|
+
* @param {string | null | undefined} gap the step's last exit gap text
|
|
247
|
+
* @returns {string} the labelled block, or '' when there is nothing to show
|
|
176
248
|
*/
|
|
177
|
-
export function
|
|
178
|
-
if (typeof gap !== 'string' || gap === '') return
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
seen.add(p);
|
|
187
|
-
const written = (writes ?? []).some((w) => {
|
|
188
|
-
const ws = String(w);
|
|
189
|
-
return ws === p || ws.endsWith(`/${p}`) || p.endsWith(`/${ws}`);
|
|
190
|
-
});
|
|
191
|
-
if (!written) out.push(p);
|
|
192
|
-
}
|
|
193
|
-
return out;
|
|
249
|
+
export function closeGapBlock(gap) {
|
|
250
|
+
if (typeof gap !== 'string' || gap.trim() === '') return '';
|
|
251
|
+
const scrubbed = redactSecrets(gap);
|
|
252
|
+
// BACKSTOP, not an envelope: over the line only. Scrubbing can only grow a string
|
|
253
|
+
// (a mask is wider than most literals it replaces), so the length is measured
|
|
254
|
+
// AFTER it — bounding on the raw length would let a gap cross the ceiling on its
|
|
255
|
+
// way through the one transformation that runs unconditionally.
|
|
256
|
+
return '\nWhat this step\'s exits reported on its last attempt (their own output, verbatim):\n'
|
|
257
|
+
+ (scrubbed.length > CHECK_GAP_MAX ? boundGap(scrubbed, REPLAN_GAP_KEEP) : scrubbed);
|
|
194
258
|
}
|
|
195
259
|
|
|
196
260
|
/**
|
|
@@ -257,7 +321,6 @@ strictly in array order. Each step (no other fields exist):
|
|
|
257
321
|
- "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (write/edit change the tree; recall/get/impact/related/recent search and navigate the repository index; compress/peek read cheaply; stash/remember/forget park and record notes across steps)
|
|
258
322
|
- "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
|
|
259
323
|
- "target": the step's deliverable path (REQUIRED when tools include write/edit), inside ${JSON.stringify(job.writeScope)}
|
|
260
|
-
- "model" (optional): ${STEP_MODELS.join(' | ')} — a cheaper tier for mechanical steps; omit for the default
|
|
261
324
|
- "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
|
|
262
325
|
A narrowed "scope" is the ONLY ground this step can write: its "target" must sit
|
|
263
326
|
inside it, and a "tree-changed" exit's scope must not be disjoint from it (one that
|
|
@@ -403,6 +466,40 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
403
466
|
* `flat` — "revise the goal" — on a run that was converging when its allowance ran
|
|
404
467
|
* out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
|
|
405
468
|
* bounds and mints none of its strikes. Empty/omitted is the cold path.
|
|
469
|
+
* @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
|
|
470
|
+
* replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
|
|
471
|
+
* `restart.replanGrantUsed`), seeding the ceiling below. `resumeGrades`' sibling and
|
|
472
|
+
* deliberately NOT its twin: a grade seed feeds a READOUT, this one feeds a BOUND.
|
|
473
|
+
*
|
|
474
|
+
* Without it the ceiling is reborn on every call, and a resume is another call:
|
|
475
|
+
* measured, a leg that spent both replans and step-redded handed its checkpoint to a
|
|
476
|
+
* second leg that replanned twice more. Two became four by being killed once, which is
|
|
477
|
+
* the creep the latch exists to make impossible ("unlimited replanning launders thrash
|
|
478
|
+
* as adaptation", PRD v1.12).
|
|
479
|
+
*
|
|
480
|
+
* WHY THIS SEED SPENDS THE LEG'S BOUND WHERE `resumeGrades` DOES NOT. src/trend.js's
|
|
481
|
+
* THE SEED refuses to seed the fix loop's ITERATIONS from history "because the leg's
|
|
482
|
+
* own bounds must not be spent by history" — and that is right, because an attempt
|
|
483
|
+
* allowance is a LEG bound: a restarted leg buys its own attempts with its own money.
|
|
484
|
+
* The replan ceiling is a RUN bound by doctrine. It bounds how many times the WORKFLOW
|
|
485
|
+
* may be redrawn before redrawing it is thrash, and that question spans exactly the
|
|
486
|
+
* chain the halt readout already spans. The two seeds therefore disagree on purpose;
|
|
487
|
+
* they are not the same rule applied inconsistently.
|
|
488
|
+
*
|
|
489
|
+
* CHAIN SEMANTICS, and the one place this differs from the grade seed's mechanism.
|
|
490
|
+
* `readGradeSeed`'s documented KNOWN LIMIT is that it reads ONE spine, so a resume of a
|
|
491
|
+
* resume inherits leg 2's grades and not leg 1's — the chain shortens by one leg per
|
|
492
|
+
* resume, which is fail-safe for a READOUT (a shorter chain can only under-claim a
|
|
493
|
+
* direction). It is the DANGEROUS direction for a ceiling: an under-claimed ledger is a
|
|
494
|
+
* refilled allowance, and every kill would buy one. So this seed does NOT reuse that
|
|
495
|
+
* mechanism. It follows the MONEY fold instead (`priorSpentUsd`, `try-start`/
|
|
496
|
+
* `job-start`): each leg DECLARES the ledger it inherited on its own spine, and the
|
|
497
|
+
* next reader adds only that leg's own `replan` records to the declared number. Leg 3
|
|
498
|
+
* therefore inherits the whole chain, not leg 2's slice.
|
|
499
|
+
*
|
|
500
|
+
* `count` seeds `replans` (and, above zero, latches `replanned`); `grantUsed` seeds the
|
|
501
|
+
* F85-C variance latch, so a killed leg cannot re-earn an extra the arbiter has already
|
|
502
|
+
* granted. Null/omitted is the cold path and is byte-identical to a run nobody resumed.
|
|
406
503
|
* @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
|
|
407
504
|
* ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
|
|
408
505
|
* cut, an inherited resume floor); the money-halt readout states it beside the remaining
|
|
@@ -417,7 +514,7 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
417
514
|
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
|
|
418
515
|
* `step-red:<id>`
|
|
419
516
|
*/
|
|
420
|
-
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [] }) {
|
|
517
|
+
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, maxStepRounds = 40, layerRoot = false, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null }) {
|
|
421
518
|
workdir = resolve(workdir);
|
|
422
519
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
423
520
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
@@ -742,7 +839,25 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
742
839
|
});
|
|
743
840
|
for (const m of menu) {
|
|
744
841
|
const v = await runStages(m.run, scrub, closeOpts);
|
|
745
|
-
if (v.verdict === 'needs_revision')
|
|
842
|
+
if (v.verdict === 'needs_revision') {
|
|
843
|
+
seedRed.push(m.name);
|
|
844
|
+
// …and the preflight grade is a READING, for the same reason the precheck one
|
|
845
|
+
// above is: it is the seed the run's work is measured against. The precheck
|
|
846
|
+
// only ever reds at the close's FIRST failing stage (first-red-wins), so on
|
|
847
|
+
// every shipped close — each of which opens with `changed-from-seed` — the
|
|
848
|
+
// numeric stages have no baseline at all without it. A step's first grade
|
|
849
|
+
// would then have nothing to compare against and the whole run would read
|
|
850
|
+
// `unknown` on exactly the stop the readout exists for.
|
|
851
|
+
//
|
|
852
|
+
// ONCE PER STAGE, though. The precheck and this loop grade the SAME unchanged
|
|
853
|
+
// tree back to back with no work in between, so a stage both reach reports the
|
|
854
|
+
// same number twice — and a repeated baseline in a series reads as an attempt
|
|
855
|
+
// that achieved nothing (`verdict 2 → 2 → 1`). That is a phantom flat step
|
|
856
|
+
// handed to a human and to the replanner, which is the exact class of false
|
|
857
|
+
// story this change exists to remove. Asked of the reader's own books, so
|
|
858
|
+
// there is no second record of what has been graded.
|
|
859
|
+
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record({ gap: v.gap });
|
|
860
|
+
}
|
|
746
861
|
emit('check-preflight', { name: m.name, verdict: v.verdict, ...(m.run.length > 1 ? { chain: m.run.map((s2) => s2.name) } : {}) });
|
|
747
862
|
const f = Object.hasOwn(CLOSE_FAULTS, v.verdict) ? CLOSE_FAULTS[v.verdict] : undefined;
|
|
748
863
|
if (f) {
|
|
@@ -797,7 +912,24 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
797
912
|
const v = await runStages(chain, scrub, closeOpts);
|
|
798
913
|
emit('check-run', { name, verdict: v.verdict, ...(v.stage && v.stage !== name ? { stage: v.stage } : {}), ...(v.exitCode !== undefined ? { exitCode: v.exitCode } : {}) });
|
|
799
914
|
if (v.verdict === 'satisfied') return { pass: true };
|
|
800
|
-
if (v.verdict === 'needs_revision')
|
|
915
|
+
if (v.verdict === 'needs_revision') {
|
|
916
|
+
// The run trend is fed by EVERY grade the arbiter renders, and a check IS an
|
|
917
|
+
// arbiter-rendered close grade: the same `runStages`, the same chain, the same
|
|
918
|
+
// stage names, so it lands in the same per-stage buckets and the accuracy law
|
|
919
|
+
// holds unchanged. Without this the reader is blind to everything a STEP ever
|
|
920
|
+
// achieves — it sees only the precheck and the outer fix loop — which is why
|
|
921
|
+
// u-msh70zla's meter, firing mid-step, had nothing to report about a step that
|
|
922
|
+
// had just gone 24 → 15 → 14.
|
|
923
|
+
//
|
|
924
|
+
// Folded HERE, on `v.gap`, and the seam is load-bearing: `evalExits` wraps this
|
|
925
|
+
// gap as `check "<name>" red: …`, and THAT line carries the word "red" with no
|
|
926
|
+
// number on it, so `readGrade` reads the wrapper instead of the wall and every
|
|
927
|
+
// step grade donates a null. Measured on u-msh70zla's own archived gaps: raw
|
|
928
|
+
// reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Same seam the
|
|
929
|
+
// Layer R note below already calls out for a check's `^`-anchored gapKeep.
|
|
930
|
+
runTrend.record({ gap: v.gap });
|
|
931
|
+
return { pass: false, gap: v.gap };
|
|
932
|
+
}
|
|
801
933
|
return { pass: false, fault: v.verdict, gap: v.detail };
|
|
802
934
|
};
|
|
803
935
|
|
|
@@ -1482,8 +1614,39 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1482
1614
|
const artifacts = [];
|
|
1483
1615
|
/** @type {{id: string, outcome: string}[]} */
|
|
1484
1616
|
const stepOutcomes = [];
|
|
1485
|
-
|
|
1486
|
-
|
|
1617
|
+
/** RESUME — the replan ledger the CHAIN has already spent, belted the way every
|
|
1618
|
+
* other declared fold entering the arbiter's arithmetic is (`priorSpentUsd`,
|
|
1619
|
+
* `priorWallMs`, `declaredRounds`): a garbage or negative seed reads as 0 rather
|
|
1620
|
+
* than as a NaN that would poison every later comparison, and a negative one must
|
|
1621
|
+
* never cancel a real replan and widen the ceiling (reuse's `> 0` belt, F-4). */
|
|
1622
|
+
const seededReplans = typeof resumeReplans?.count === 'number' && Number.isFinite(resumeReplans.count) && resumeReplans.count > 0
|
|
1623
|
+
? Math.floor(resumeReplans.count) : 0;
|
|
1624
|
+
// Seeded, not reset. These three were locals reborn on every `runPlan` call — and a
|
|
1625
|
+
// resume IS another call, so the ceiling was refilled by every kill (see the
|
|
1626
|
+
// `resumeReplans` param doc for the measurement and for why this bound spans the
|
|
1627
|
+
// chain while the trend's iteration bound deliberately does not).
|
|
1628
|
+
let replanned = seededReplans > 0;
|
|
1629
|
+
/** F85 (C): how many replans this run has actually drafted, and whether the
|
|
1630
|
+
* arbiter has already spent its ONE extra grant. Two variables and not one
|
|
1631
|
+
* counter compared against a limit, deliberately — a latch cannot be widened by
|
|
1632
|
+
* arithmetic, and the ceiling is the thing this rung must not move (v1.12). */
|
|
1633
|
+
let replans = seededReplans;
|
|
1634
|
+
// the F85-C latch travels BESIDE the count and is not derived from it: a leg can
|
|
1635
|
+
// inherit a spent ordinary ceiling with the grant still unearned (count 1, false),
|
|
1636
|
+
// and deriving one from the other would collapse those two states into one.
|
|
1637
|
+
let varianceGrantUsed = resumeReplans?.grantUsed === true;
|
|
1638
|
+
// `replanned` stays the shipped boolean this record has always carried; `replans`
|
|
1639
|
+
// rides BESIDE it (append-only — a field's meaning is never repurposed).
|
|
1640
|
+
//
|
|
1641
|
+
// Both now count the CHAIN rather than the leg, and that IS a meaning change — made
|
|
1642
|
+
// rather than shadowed by a second field because `replans` shipped only on this
|
|
1643
|
+
// unreleased branch, so no archived spine carries the leg reading for a reader to
|
|
1644
|
+
// be confused by. The leg's own number is not lost by it: a leg emits one `replan`
|
|
1645
|
+
// record per replan it drafts, so `chain − records-in-this-window` is the fold it
|
|
1646
|
+
// inherited, and that is exactly the arithmetic `readResume` runs to rebuild the
|
|
1647
|
+
// ledger for the next leg. A second field stating a number already derivable from
|
|
1648
|
+
// the window would be a second reader of one question (F6's structural cousin).
|
|
1649
|
+
const planExecuted = () => emit('plan-executed', { steps: stepOutcomes, replanned, replans });
|
|
1487
1650
|
/** @type {object|null} W-2 at the STEP site — set when the WALL refused to open a
|
|
1488
1651
|
* step's attempt. It carries that stop's own record fields, and its presence is what
|
|
1489
1652
|
* lets the step loop's terminal tell this reading apart from F64's mid-call one
|
|
@@ -1558,15 +1721,41 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1558
1721
|
const moneyShare = stepStartUsd > 0 ? (stepStartUsd - remainingUsd()) / stepStartUsd : 0;
|
|
1559
1722
|
const timeShare = clock.bounded && stepStartMs > 0 ? (stepStartMs - clock.remainingMs()) / stepStartMs : 0;
|
|
1560
1723
|
if (moneyShare >= VARIANCE_THRESHOLD || timeShare >= VARIANCE_THRESHOLD) {
|
|
1724
|
+
// F85 — the meter REPORTS progress; it does not decide on it. The
|
|
1725
|
+
// condition above is the whole trigger and stays the whole trigger: a
|
|
1726
|
+
// converging step that is eating the run is still stopped, because this is
|
|
1727
|
+
// a governance instrument over an operator-owned allowance and a governor
|
|
1728
|
+
// that suppressed itself whenever the work looked promising would be
|
|
1729
|
+
// judging capability with the budget. What was missing was never a term in
|
|
1730
|
+
// the condition — it was the READING. u-msh70zla stopped a step whose close
|
|
1731
|
+
// had gone 24 → 15 → 14 and then told the replanner its exits were
|
|
1732
|
+
// "unmoved", so the replanner threw the progress away and re-targeted a
|
|
1733
|
+
// file that was already clean.
|
|
1734
|
+
//
|
|
1735
|
+
// The reading comes off `runTrend` — the SAME instance the money halt reads
|
|
1736
|
+
// (src/trend.js's ONE PER SERIES). A second reader for one question is how
|
|
1737
|
+
// two readers come to disagree, which is precisely the defect being fixed.
|
|
1738
|
+
// It is per-stage, numbers-and-stage-names only, and `unknown` stays
|
|
1739
|
+
// `unknown` — a close that reports no number donates nothing (F6).
|
|
1740
|
+
const t = runTrend.verdict();
|
|
1561
1741
|
emit('variance', {
|
|
1562
1742
|
step: step.id, iteration, threshold: VARIANCE_THRESHOLD,
|
|
1563
1743
|
moneyShare: Number(moneyShare.toFixed(3)),
|
|
1564
1744
|
timeShare: clock.bounded ? Number(timeShare.toFixed(3)) : null,
|
|
1565
1745
|
axis: moneyShare >= VARIANCE_THRESHOLD ? 'money' : 'time',
|
|
1746
|
+
// ADDED fields, never repurposed ones (the spine is append-only forever).
|
|
1747
|
+
// Same four names and same shapes the money halt already emits, so one
|
|
1748
|
+
// reader parses a progress reading wherever it appears.
|
|
1749
|
+
trend: t.trend, motion: t.motion, reading: t.reading, series: t.series,
|
|
1566
1750
|
});
|
|
1567
1751
|
const err = /** @type {CategorizedError} */ (new Error(
|
|
1568
1752
|
`step "${step.id}" consumed ${Math.round(Math.max(moneyShare, timeShare) * 100)}% of the run's remaining `
|
|
1569
|
-
+ `${moneyShare >= VARIANCE_THRESHOLD ? 'money' : 'time'} across ${iteration - 1} attempt(s) with its exits still red`
|
|
1753
|
+
+ `${moneyShare >= VARIANCE_THRESHOLD ? 'money' : 'time'} across ${iteration - 1} attempt(s) with its exits still red. `
|
|
1754
|
+
// The escalation `detail` is this message (ralph passes it through), and
|
|
1755
|
+
// the replan brief quotes the detail — so this sentence is what a human
|
|
1756
|
+
// AND the redrafting planner read. It states what the run achieved; it
|
|
1757
|
+
// never states what to do about it, which is the operator's lever list.
|
|
1758
|
+
+ `Run progress: ${t.reading}.`));
|
|
1570
1759
|
err.category = 'step-variance';
|
|
1571
1760
|
throw err;
|
|
1572
1761
|
}
|
|
@@ -1772,12 +1961,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1772
1961
|
// pricing-red at the step-end guard above, so isUnpriced() is false here.)
|
|
1773
1962
|
//
|
|
1774
1963
|
// A (PRD v1.27) adds the SECOND trigger: `step-variance` — the meter stopped a
|
|
1775
|
-
// step that had eaten a declared share of the run
|
|
1776
|
-
//
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
// replan ceiling — this changes the trigger, never the ceiling (v1.12).
|
|
1964
|
+
// step that had eaten a declared share of the run. That is the case
|
|
1965
|
+
// exhaustion-only could never reach (F56: a replan has fired zero times in the
|
|
1966
|
+
// programme), and it is a genuine re-allocation rather than a stop. Both
|
|
1967
|
+
// triggers still need FUNDS LEFT, and both are bounded by the replan ceiling.
|
|
1780
1968
|
const cat = lastEscalation?.category;
|
|
1969
|
+
/** F85 — the run's measured trajectory at the moment of this stop, read ONCE
|
|
1970
|
+
* here off the same instance the meter and the money halt read. Both consumers
|
|
1971
|
+
* below are the replanner's: the trigger sentence and the brief. */
|
|
1972
|
+
const stopTrend = runTrend.verdict();
|
|
1781
1973
|
// F66 adds the THIRD trigger: `step-stalled` — the stall watchdog reissued the
|
|
1782
1974
|
// model call MAX_STALLS times and never got a round back. hamr's ruling is that
|
|
1783
1975
|
// a stall must self-heal rather than end the run ("killing and coming back is
|
|
@@ -1785,13 +1977,58 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1785
1977
|
// demonstrably stopped working does the run adapt instead — by replanning, not
|
|
1786
1978
|
// by stopping. Same ONE-replan ceiling and same FUNDS-LEFT condition as the
|
|
1787
1979
|
// other two: this changes the trigger, never the ceiling (v1.12).
|
|
1980
|
+
//
|
|
1981
|
+
// F85 rewrote the variance branch. It used to be the FIXED string "…with its
|
|
1982
|
+
// exits unmoved", printed for every step-variance stop whatever had happened —
|
|
1983
|
+
// and on u-msh70zla it was simply false: the exits had moved 24 → 15 → 14. The
|
|
1984
|
+
// sentence now states the meter's fact (a share of the run) and then the trend
|
|
1985
|
+
// instrument's MEASURED reading, which is the same reading the `variance` record
|
|
1986
|
+
// and the money halt carry. Nothing here asserts a direction the reader did not
|
|
1987
|
+
// report: `unknown` says unknown.
|
|
1788
1988
|
const replanTrigger = cat === 'cap-halt' ? 'step exhausted its attempts with exits still red'
|
|
1789
|
-
: cat === 'step-variance' ?
|
|
1989
|
+
: cat === 'step-variance' ? `the meter stopped a step that was consuming the run — ${stopTrend.reading}`
|
|
1790
1990
|
: cat === 'step-stalled' ? 'the model stopped producing rounds and reissuing the call did not recover it'
|
|
1791
1991
|
: null;
|
|
1792
|
-
|
|
1992
|
+
// F85 (C) — the ARBITER's one additional replan. A second `step-variance` after
|
|
1993
|
+
// the ceiling is spent used to be a hard stop, and on a run that was still
|
|
1994
|
+
// converging that stop threw away work the run had already paid for. So the
|
|
1995
|
+
// arbiter may grant ONE more, and every clause of that sentence is a constraint:
|
|
1996
|
+
//
|
|
1997
|
+
// * ONE. Bounded, and bounded by a latch rather than a comparison, so the
|
|
1998
|
+
// ceiling cannot creep: unlimited replanning launders thrash as adaptation
|
|
1999
|
+
// (v1.12). A third variance stop is the stop, however well it is going.
|
|
2000
|
+
// * the ARBITER. Read mechanically off `runTrend` — the same instance the
|
|
2001
|
+
// meter and the money halt read. The agent never asks for it, never sees it
|
|
2002
|
+
// offered, and cannot influence it: no self-adjusted budgets, ever.
|
|
2003
|
+
// * `converging`, the trend instrument's OWN category. No new number is
|
|
2004
|
+
// invented here — threshold-setting is the operator's, and a fresh
|
|
2005
|
+
// comparison would be a second reader of the one question (src/trend.js).
|
|
2006
|
+
// `flat` and `unknown` are refusals; an unknown is never rounded up into a
|
|
2007
|
+
// reason to spend (F6).
|
|
2008
|
+
// * step-variance only. An exhaustion or a stall after the ceiling is
|
|
2009
|
+
// unchanged — this widens nothing but the case it was measured on.
|
|
2010
|
+
const grantExtraReplan = replanned
|
|
2011
|
+
&& cat === 'step-variance'
|
|
2012
|
+
&& !varianceGrantUsed
|
|
2013
|
+
&& stopTrend.trend === 'converging';
|
|
2014
|
+
if ((!replanned || grantExtraReplan) && replanTrigger && remainingUsd() > MONEY_MIN) {
|
|
2015
|
+
if (grantExtraReplan) varianceGrantUsed = true;
|
|
1793
2016
|
replanned = true;
|
|
1794
|
-
|
|
2017
|
+
replans += 1;
|
|
2018
|
+
emit('replan', {
|
|
2019
|
+
step: step.id, reason: replanTrigger, trigger: cat,
|
|
2020
|
+
// ADDED fields (append-only): which replan this is, and — only when the
|
|
2021
|
+
// arbiter granted one past the ordinary ceiling — the reading it granted it
|
|
2022
|
+
// on. Absent on the ordinary replan, so a reader can never mistake the
|
|
2023
|
+
// ceiling for a grant.
|
|
2024
|
+
//
|
|
2025
|
+
// The index is the RUN's, so on a resumed leg it opens above 1 — the ceiling
|
|
2026
|
+
// it is counted against spans the chain, and an index that restarted at 1 on
|
|
2027
|
+
// every leg would read as a fresh allowance on the one record that could have
|
|
2028
|
+
// shown it was not.
|
|
2029
|
+
replan: replans,
|
|
2030
|
+
...(grantExtraReplan ? { granted: stopTrend.trend } : {}),
|
|
2031
|
+
});
|
|
1795
2032
|
// The brief names the trigger HONESTLY, one branch per trigger: it is the
|
|
1796
2033
|
// only channel the redrafting planner adapts to, so a wrong diagnosis here
|
|
1797
2034
|
// is handed to the one component whose whole job is to respond to it. A
|
|
@@ -1809,28 +2046,45 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1809
2046
|
const leftOver = `$${remainingUsd().toFixed(2)} and `
|
|
1810
2047
|
+ `${clock.bounded ? `${Math.round(clock.remainingMs() / 60_000)} minute(s)` : 'time UNBOUNDED'} `
|
|
1811
2048
|
+ 'of the run were still unspent when it stopped.';
|
|
2049
|
+
// F85: the meter's branch states the METER's fact and then the run's measured
|
|
2050
|
+
// one, in that order and as two separate claims — "it was stopped for eating
|
|
2051
|
+
// the run" and "here is what the run achieved" are different things, and the
|
|
2052
|
+
// old sentence quietly fused them into a verdict on the work.
|
|
1812
2053
|
const why = cat === 'step-variance'
|
|
1813
|
-
? 'It was stopped by the run\'s meter for consuming too large a share of what was left.'
|
|
2054
|
+
? 'It was stopped by the run\'s meter for consuming too large a share of what was left. '
|
|
2055
|
+
+ `Its work so far, from the close's own numbers: ${stopTrend.reading}. ${leftOver}`
|
|
1814
2056
|
: cat === 'step-stalled'
|
|
1815
2057
|
? 'It stalled: the model stopped producing rounds and reissuing the call did not recover it, so its exits were never judged.'
|
|
1816
2058
|
: `${res.ladder.brief()} ${leftOver}`;
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2059
|
+
// F86 — this step's OWN last exit results, verbatim (u-mshcpdg4). Everything
|
|
2060
|
+
// above this line is the run's narration of the stop: the meter's share
|
|
2061
|
+
// sentence, the trend's direction, the never-wrote fact, the escalation detail.
|
|
2062
|
+
// None of them says WHERE the remaining work is, because none of them is the
|
|
2063
|
+
// close. The close's output is in HERE — as one of the failing exits' details,
|
|
2064
|
+
// beside the evaluator's own (a step that wrote nothing leads with
|
|
2065
|
+
// `tree-changed`), which is why the block is labelled by its exits rather than
|
|
2066
|
+
// by the close. It goes over as text — see `closeGapBlock` for why a parsed
|
|
2067
|
+
// file list would reproduce the exact bug it is fixing. Empty gap → empty
|
|
2068
|
+
// string → the brief is byte-identical to the pre-F86 one.
|
|
2069
|
+
const gapBlock = closeGapBlock(res?.gap);
|
|
1824
2070
|
const failure = `Step "${step.id}" (${step.action}) did not reach its exits. `
|
|
1825
2071
|
+ `${why}\n`
|
|
1826
|
-
+ missedLine
|
|
1827
2072
|
+ `Last exit state:\n${lastEscalation?.detail ?? '(none)'}\n`
|
|
1828
|
-
+ `Steps completed so far: ${artifacts.map((a) => a.id).join(', ') || 'none'}
|
|
2073
|
+
+ `Steps completed so far: ${artifacts.map((a) => a.id).join(', ') || 'none'}.`
|
|
2074
|
+
+ gapBlock;
|
|
1829
2075
|
// The progress line IS the adaptation channel (addendum 2): the balance rides
|
|
1830
2076
|
// in via obtainPlan's live read, and this says where the run got to. Both are
|
|
1831
2077
|
// the planner re-allocating what remains across what is left — never a rate.
|
|
2078
|
+
//
|
|
2079
|
+
// F85 adds the second half. This line used to count STEPS only ("step 1 of 1
|
|
2080
|
+
// did not finish; 0 step(s) completed before it") — structurally true and
|
|
2081
|
+
// silent about outcome, so a replanner reading it could only conclude that
|
|
2082
|
+
// nothing had been achieved. On u-msh70zla it concluded exactly that and threw
|
|
2083
|
+
// three attempts of real convergence away. The structural sentence stays (a
|
|
2084
|
+
// plan's shape is what the planner re-allocates); the measured one joins it.
|
|
1832
2085
|
const progress = `step ${idx + 1} of ${plan.steps.length} ("${step.id}") did not finish; `
|
|
1833
|
-
+ `${artifacts.length} step(s) completed before it
|
|
2086
|
+
+ `${artifacts.length} step(s) completed before it; `
|
|
2087
|
+
+ `close trend so far: ${stopTrend.reading}`;
|
|
1834
2088
|
let pv;
|
|
1835
2089
|
try {
|
|
1836
2090
|
pv = await obtainPlan('replan', failure, progress);
|
|
@@ -1886,8 +2140,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1886
2140
|
emitMoneyHalt({ phase: `step:${step.id}`, cutMidCall: false, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
1887
2141
|
return 'cap-halt';
|
|
1888
2142
|
}
|
|
1889
|
-
// A
|
|
1890
|
-
//
|
|
2143
|
+
// A `step-variance` that earned no replan above is a STOP, and the stop is the
|
|
2144
|
+
// result. Reached in three ways now: the ordinary ceiling with the run reading
|
|
2145
|
+
// `flat` or `unknown`; the arbiter's one grant already spent (F85's bound); or
|
|
2146
|
+
// the wallet at dust. Each rides out as `step-red:<id>`, NOT as its own top-level
|
|
1891
2147
|
// outcome. The category is a replan TRIGGER, never a run verdict: leaking it
|
|
1892
2148
|
// upward would mint an outcome run.js and the ledger's class table do not know,
|
|
1893
2149
|
// and an unmapped category is counted as a library bug (ledger.js) when this is a
|
package/src/ralph.js
CHANGED
|
@@ -359,10 +359,17 @@ export const GAP_KEEP_TRIM_MARKER = 'more elided by the';
|
|
|
359
359
|
* `keepPattern` is set (F28), lines of the WHOLE stream matching it are also
|
|
360
360
|
* carried in a clearly-delimited block between head and tail — capped — so the
|
|
361
361
|
* failing-test names buried in the middle still reach the worker.
|
|
362
|
+
* Exported for the ONE reason `GAP_KEEP_TRIM_MARKER` above is: a second consumer
|
|
363
|
+
* that needs a gap bounded must reuse THIS envelope, not spell its own. A private
|
|
364
|
+
* copy would be a second truncation scheme, and the two would drift on exactly the
|
|
365
|
+
* property that matters — whether the red lines survive, and whether the trim
|
|
366
|
+
* announces itself (F28: silent truncation is the disease this function cures).
|
|
367
|
+
* The close path is unchanged: `runClose` still calls it with the stage's own
|
|
368
|
+
* `gapKeep`, byte-identically.
|
|
362
369
|
* @param {string} s the (redacted) close output
|
|
363
370
|
* @param {string} [keepPattern] regex source; matching lines survive the elision
|
|
364
371
|
*/
|
|
365
|
-
function boundGap(s, keepPattern) {
|
|
372
|
+
export function boundGap(s, keepPattern) {
|
|
366
373
|
if (s.length <= GAP_HEAD + GAP_TAIL + 100) return s;
|
|
367
374
|
const head = s.slice(0, GAP_HEAD);
|
|
368
375
|
const tail = s.slice(-GAP_TAIL);
|