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/CHANGELOG.md +704 -0
- package/README.md +13 -4
- package/bareloop.context.md +351 -32
- package/package.json +1 -1
- package/src/authorflow.js +1280 -0
- package/src/authoring.js +1296 -0
- package/src/authorjob.js +793 -0
- package/src/authorscout.js +706 -0
- package/src/bridges.js +2 -2
- package/src/declaredclose.js +519 -0
- package/src/exits.js +8 -2
- package/src/index.js +72 -0
- package/src/job.js +91 -10
- package/src/kinds.js +1354 -0
- package/src/ledger.js +70 -9
- package/src/plan.js +57 -101
- package/src/planrun.js +554 -75
- package/src/ralph.js +27 -5
- package/src/reuse.js +135 -11
- package/src/root.js +8 -1
- package/src/run.js +29 -3
- package/src/text.js +120 -0
- package/src/validate.js +121 -0
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +720 -0
- package/types/authoring.d.ts +447 -0
- package/types/authorjob.d.ts +241 -0
- package/types/authorscout.d.ts +307 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +14 -2
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +41 -38
- package/types/planrun.d.ts +125 -18
- package/types/ralph.d.ts +17 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +23 -2
- package/types/text.d.ts +84 -0
- package/types/validate.d.ts +50 -0
- package/types/workbranch.d.ts +102 -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';
|
|
23
|
-
import { validatePlan, legalScopes,
|
|
24
|
-
import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU
|
|
25
|
-
import { snapshotScope, evalExits } from './exits.js';
|
|
22
|
+
import { runClose, runStages, ralph, CLOSE_FAULTS, boundGap } from './ralph.js';
|
|
23
|
+
import { validatePlan, legalScopes, closeStagesOf } from './plan.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';
|
|
@@ -33,6 +33,9 @@ import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
|
33
33
|
import { validateBridge, loadGate } from './bridges.js';
|
|
34
34
|
import { extractArtifact } from './text.js';
|
|
35
35
|
import { createClock, isWallTimeout } from './clock.js';
|
|
36
|
+
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade } from './declaredclose.js';
|
|
37
|
+
import { seedAtHead, seedListing } from './kinds.js';
|
|
38
|
+
import { workBranchName, prepareWorkBranch } from './workbranch.js';
|
|
36
39
|
|
|
37
40
|
const require = createRequire(import.meta.url);
|
|
38
41
|
const { Loop, wireGate, HaltError } = require('bare-agent');
|
|
@@ -157,40 +160,104 @@ required above, and nothing else.
|
|
|
157
160
|
${JSON.stringify(plan, null, 2)}`;
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
/** The red-set for the replan brief's copy of a step's exit gap: every non-blank
|
|
164
|
+
* line. It is `\S` and not the close stage's own `gapKeep` for the reason Layer R
|
|
165
|
+
* already pays at the step seam (see `createRoot` below) — a shipped `gapKeep` is
|
|
166
|
+
* `^`-anchored (`^BAREAGENT `, `^red`, `^FAILED`) and the exit evaluator wraps the
|
|
167
|
+
* stage's output in `check "x" red: …`, so the anchor no longer sits where the
|
|
168
|
+
* pattern expects it. A gap that reaches here is ALREADY a red-set: `judge` builds
|
|
169
|
+
* it out of the failing exits only, so every line in it is a red line and keeping
|
|
170
|
+
* all of them is the correct instrument, not a widened one. */
|
|
171
|
+
const REPLAN_GAP_KEEP = '\\S';
|
|
172
|
+
|
|
160
173
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
174
|
+
* This step's own last EXIT results, rendered for the REPLAN brief.
|
|
175
|
+
*
|
|
176
|
+
* The label says exits and not "the close", and the distinction is the whole of one
|
|
177
|
+
* fix: `res.gap` is `lastGap`, the join of EVERY failing exit's detail in exit order.
|
|
178
|
+
* With `MAX_EXITS_PER_STEP` at 2 and the mandatory `tree-changed` pairing on a write
|
|
179
|
+
* step, a step that wrote nothing leads the block with the EVALUATOR's own prose
|
|
180
|
+
* (`0 files changed under src/** …`) — captured verbatim from a real replan prompt,
|
|
181
|
+
* under a label reading "the verification's own output". The close stage's output is
|
|
182
|
+
* one of the details here, never reliably all of them. The payload is left alone: "the
|
|
183
|
+
* step wrote nothing" is exactly what a replanner should be told, and stripping it to
|
|
184
|
+
* make an old label true would trade a real fact for a tidy sentence.
|
|
185
|
+
*
|
|
186
|
+
* u-mshcpdg4: a run one strict error from done, killed with $1.10 and 82 seconds
|
|
187
|
+
* unspent. The close named the remaining work exactly — `src/recurse.js(978,115)`
|
|
188
|
+
* — and the worker read that every attempt. The REPLANNER, which is the component
|
|
189
|
+
* that chooses which files the next plan targets, was handed only the trend line
|
|
190
|
+
* (`still progressing — typecheck 30 → 15 → 15 → 1`): converging, and silent about
|
|
191
|
+
* WHERE. It re-targeted `src/loop.js`, already at zero errors; that step wrote
|
|
192
|
+
* nothing and struck out. F85 gave the brief the trajectory; this gives it the
|
|
193
|
+
* artifact the trajectory is a summary OF.
|
|
194
|
+
*
|
|
195
|
+
* Handed over as TEXT, never as a parsed file list, and the trap is worth naming
|
|
196
|
+
* because the parse is the obvious-looking fix: line 1 of that very gap reads
|
|
197
|
+
* `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and
|
|
198
|
+
* only line 2 names the culprit. The `never wrote` advisory that used to sit here
|
|
199
|
+
* did exactly that parse, and on this gap it resolved to `src/loop.js` — the file
|
|
200
|
+
* already CLEAN — then said so as a directive beside the artifact that said the
|
|
201
|
+
* opposite. It was deleted with this change (hamr, 2026-08-05): two readers of one
|
|
202
|
+
* question, the parsed one wrong, is the same failure the variance meter just had.
|
|
203
|
+
* Nothing generic can tell a summary line from a detail line, and the
|
|
204
|
+
* shapes differ per close anyway (tsc file:line, pytest test ids, a count close
|
|
205
|
+
* that names no file at all). A model reading the artifact can make that
|
|
206
|
+
* distinction; a regex reproduces the bug. So: no extraction, no digest, no
|
|
207
|
+
* inference — the artifact, bounded and scrubbed.
|
|
208
|
+
*
|
|
209
|
+
* Two boundaries, both reused rather than respelled:
|
|
210
|
+
* - SCRUB. A prompt is an egress point and this repo has ONE secret inventory
|
|
211
|
+
* (`SECRET_PATTERNS` via `redactSecrets`). The gap arrives already scrubbed
|
|
212
|
+
* from `judge`, which makes this defense in depth rather than the only guard —
|
|
213
|
+
* and defense in depth at the egress is exactly where it belongs, because the
|
|
214
|
+
* next caller of this helper will not remember the upstream one.
|
|
215
|
+
* - BOUND, and it is a BACKSTOP rather than an envelope. What arrives here has
|
|
216
|
+
* ALREADY been bounded: `runClose` ran `boundGap` with the stage's own `gapKeep`,
|
|
217
|
+
* which deliberately rescues the `not ok`/`FAILED` names out of the elided middle
|
|
218
|
+
* — the mechanical gap F46's conversion mechanism feeds the worker. Re-bounding
|
|
219
|
+
* that with a fresh 400/1500 envelope deletes exactly those names a second time,
|
|
220
|
+
* which is F28 reintroduced; measured on a 120-red-line close output, the first
|
|
221
|
+
* bound rescued 78 names and this one dropped 12 of them. src/exits.js states the
|
|
222
|
+
* same rule for the same artifact one seam earlier, and this now uses ITS number:
|
|
223
|
+
* `CHECK_GAP_MAX`, imported rather than respelled, so the two seams cannot drift
|
|
224
|
+
* on how big a check gap may be. Under it the gap passes through VERBATIM.
|
|
225
|
+
*
|
|
226
|
+
* A backstop is still needed, and the headroom is why the ceiling is not smaller.
|
|
227
|
+
* `res.gap` is `lastGap`, the JOIN of up to `MAX_EXITS_PER_STEP` failing exit
|
|
228
|
+
* details, and the check-passes branch slices EACH at `CHECK_GAP_MAX` — so what
|
|
229
|
+
* arrives is bounded per detail, never in total. Measured, the real maximum is
|
|
230
|
+
* comfortably under: `runClose`'s own envelope tops out at ~10.1KB (400 head +
|
|
231
|
+
* 8192 keep + 1500 tail + labels), and joined with a `tree-changed` line that is
|
|
232
|
+
* ~10.2KB, so the shipped path passes through untouched with ~1.8KB to spare. The
|
|
233
|
+
* backstop therefore fires only where src/exits.js says it should: a seam that
|
|
234
|
+
* returned something never bounded at all.
|
|
235
|
+
*
|
|
236
|
+
* Over the line `boundGap` runs with `REPLAN_GAP_KEEP`, the same envelope the
|
|
237
|
+
* close path uses, and every trim announces itself (F28). Note what that keep-set
|
|
238
|
+
* cannot do: `\S` matches every non-blank line, so the keep block degenerates into
|
|
239
|
+
* "the first 50 lines" and re-surfaces nothing from the middle. That is the honest
|
|
240
|
+
* limit of a red-set that cannot know the stage's own pattern (see
|
|
241
|
+
* `REPLAN_GAP_KEEP`), and it is affordable precisely because it now applies only
|
|
242
|
+
* to a gap nothing upstream ever bounded — on that gap ANY envelope loses names,
|
|
243
|
+
* and the choice is a bounded prompt over an unbounded one.
|
|
244
|
+
*
|
|
245
|
+
* No gap → the empty string, so the brief renders byte-identically to the pre-F86
|
|
246
|
+
* one. A labelled empty section would be an invitation to explain an absence the
|
|
247
|
+
* run never observed (a stall never judged its exits at all).
|
|
172
248
|
*
|
|
173
|
-
* @param {string | undefined} gap the step's last exit gap text
|
|
174
|
-
* @
|
|
175
|
-
* @returns {string[]} gap-named files with no matching write, in gap order
|
|
249
|
+
* @param {string | null | undefined} gap the step's last exit gap text
|
|
250
|
+
* @returns {string} the labelled block, or '' when there is nothing to show
|
|
176
251
|
*/
|
|
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;
|
|
252
|
+
export function closeGapBlock(gap) {
|
|
253
|
+
if (typeof gap !== 'string' || gap.trim() === '') return '';
|
|
254
|
+
const scrubbed = redactSecrets(gap);
|
|
255
|
+
// BACKSTOP, not an envelope: over the line only. Scrubbing can only grow a string
|
|
256
|
+
// (a mask is wider than most literals it replaces), so the length is measured
|
|
257
|
+
// AFTER it — bounding on the raw length would let a gap cross the ceiling on its
|
|
258
|
+
// way through the one transformation that runs unconditionally.
|
|
259
|
+
return '\nWhat this step\'s exits reported on its last attempt (their own output, verbatim):\n'
|
|
260
|
+
+ (scrubbed.length > CHECK_GAP_MAX ? boundGap(scrubbed, REPLAN_GAP_KEEP) : scrubbed);
|
|
194
261
|
}
|
|
195
262
|
|
|
196
263
|
/**
|
|
@@ -242,7 +309,7 @@ export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes,
|
|
|
242
309
|
// executes and `validatePlan` accepts against. Reading `job.close` raw here left
|
|
243
310
|
// the legacy object form offering nothing while the runner preflighted a stage.
|
|
244
311
|
// (`?? []`: a close naming no command stages nothing, so it offers nothing)
|
|
245
|
-
const closeStages =
|
|
312
|
+
const closeStages = closeStagesOf(job) ?? [];
|
|
246
313
|
const checkNames = checkMenu(closeStages).map((m) => m.name);
|
|
247
314
|
const doc = `DRAFT-PLAN
|
|
248
315
|
You are planning how to accomplish a goal in a repository, as an ordered list of bounded
|
|
@@ -257,7 +324,6 @@ strictly in array order. Each step (no other fields exist):
|
|
|
257
324
|
- "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
325
|
- "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
|
|
259
326
|
- "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
327
|
- "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
|
|
262
328
|
A narrowed "scope" is the ONLY ground this step can write: its "target" must sit
|
|
263
329
|
inside it, and a "tree-changed" exit's scope must not be disjoint from it (one that
|
|
@@ -403,6 +469,40 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
403
469
|
* `flat` — "revise the goal" — on a run that was converging when its allowance ran
|
|
404
470
|
* out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
|
|
405
471
|
* bounds and mints none of its strikes. Empty/omitted is the cold path.
|
|
472
|
+
* @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
|
|
473
|
+
* replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
|
|
474
|
+
* `restart.replanGrantUsed`), seeding the ceiling below. `resumeGrades`' sibling and
|
|
475
|
+
* deliberately NOT its twin: a grade seed feeds a READOUT, this one feeds a BOUND.
|
|
476
|
+
*
|
|
477
|
+
* Without it the ceiling is reborn on every call, and a resume is another call:
|
|
478
|
+
* measured, a leg that spent both replans and step-redded handed its checkpoint to a
|
|
479
|
+
* second leg that replanned twice more. Two became four by being killed once, which is
|
|
480
|
+
* the creep the latch exists to make impossible ("unlimited replanning launders thrash
|
|
481
|
+
* as adaptation", PRD v1.12).
|
|
482
|
+
*
|
|
483
|
+
* WHY THIS SEED SPENDS THE LEG'S BOUND WHERE `resumeGrades` DOES NOT. src/trend.js's
|
|
484
|
+
* THE SEED refuses to seed the fix loop's ITERATIONS from history "because the leg's
|
|
485
|
+
* own bounds must not be spent by history" — and that is right, because an attempt
|
|
486
|
+
* allowance is a LEG bound: a restarted leg buys its own attempts with its own money.
|
|
487
|
+
* The replan ceiling is a RUN bound by doctrine. It bounds how many times the WORKFLOW
|
|
488
|
+
* may be redrawn before redrawing it is thrash, and that question spans exactly the
|
|
489
|
+
* chain the halt readout already spans. The two seeds therefore disagree on purpose;
|
|
490
|
+
* they are not the same rule applied inconsistently.
|
|
491
|
+
*
|
|
492
|
+
* CHAIN SEMANTICS, and the one place this differs from the grade seed's mechanism.
|
|
493
|
+
* `readGradeSeed`'s documented KNOWN LIMIT is that it reads ONE spine, so a resume of a
|
|
494
|
+
* resume inherits leg 2's grades and not leg 1's — the chain shortens by one leg per
|
|
495
|
+
* resume, which is fail-safe for a READOUT (a shorter chain can only under-claim a
|
|
496
|
+
* direction). It is the DANGEROUS direction for a ceiling: an under-claimed ledger is a
|
|
497
|
+
* refilled allowance, and every kill would buy one. So this seed does NOT reuse that
|
|
498
|
+
* mechanism. It follows the MONEY fold instead (`priorSpentUsd`, `try-start`/
|
|
499
|
+
* `job-start`): each leg DECLARES the ledger it inherited on its own spine, and the
|
|
500
|
+
* next reader adds only that leg's own `replan` records to the declared number. Leg 3
|
|
501
|
+
* therefore inherits the whole chain, not leg 2's slice.
|
|
502
|
+
*
|
|
503
|
+
* `count` seeds `replans` (and, above zero, latches `replanned`); `grantUsed` seeds the
|
|
504
|
+
* F85-C variance latch, so a killed leg cannot re-earn an extra the arbiter has already
|
|
505
|
+
* granted. Null/omitted is the cold path and is byte-identical to a run nobody resumed.
|
|
406
506
|
* @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
|
|
407
507
|
* ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
|
|
408
508
|
* cut, an inherited resume floor); the money-halt readout states it beside the remaining
|
|
@@ -412,12 +512,18 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
412
512
|
* default; a caller supplies this to drive time deterministically (the same seam `createClock`
|
|
413
513
|
* already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
|
|
414
514
|
* whose floor is one close timeout).
|
|
515
|
+
* @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed leg
|
|
516
|
+
* recorded on its own spine (`readResume`'s `restart.branch`, off the `work-branch`
|
|
517
|
+
* event). With it the resume returns to the branch its own work is sitting on; without
|
|
518
|
+
* it the deterministic name would collide with that very branch and the collision walk
|
|
519
|
+
* would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
|
|
520
|
+
* path. A recorded branch that no longer exists is a STOP, never a fresh start.
|
|
415
521
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
416
522
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
417
|
-
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
418
|
-
* `step-red:<id>`
|
|
523
|
+
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
524
|
+
* 'step-stalled' | `step-red:<id>`
|
|
419
525
|
*/
|
|
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 = [] }) {
|
|
526
|
+
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, resumeBranch = null }) {
|
|
421
527
|
workdir = resolve(workdir);
|
|
422
528
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
423
529
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
@@ -453,14 +559,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
453
559
|
// A STAGED close (PRD v1.28) is an ordered list of command stages and is the
|
|
454
560
|
// go-forward shape; the object form survives only for the declared-but-locked
|
|
455
561
|
// verdict classes (gold/rubric/hitl), which name no command to run.
|
|
456
|
-
// W4: the staging itself lives in ONE place (`
|
|
562
|
+
// W4: the staging itself lives in ONE place (`closeStagesOf`), shared with
|
|
457
563
|
// `planPrompt` and `validatePlan` — a second copy here is exactly how the
|
|
458
|
-
// runner came to execute a stage neither of the other two could see.
|
|
459
|
-
|
|
564
|
+
// runner came to execute a stage neither of the other two could see. M4 widened
|
|
565
|
+
// that one derivation to the AUTHORED close (`closeDecl`) rather than adding a
|
|
566
|
+
// second: a consumer reading only `close` sees a declared job as closeless.
|
|
567
|
+
const stagedClose = closeStagesOf(job);
|
|
568
|
+
const declared = isDeclaredClose(job);
|
|
460
569
|
if (!stagedClose) {
|
|
461
570
|
emit('escalation', {
|
|
462
571
|
category: 'close-unsupported', decisionReady: true,
|
|
463
|
-
decision: `The job's close is a ${job.close
|
|
572
|
+
decision: `The job's close is a ${job.close?.type ?? 'non-command'} close — the plan flow executes commands whose exit codes are truth (a staged close, a single predicate, or an authored declaration).`,
|
|
464
573
|
options: ['restate the close as a predicate', 'wait for the verdict-classes rung'],
|
|
465
574
|
});
|
|
466
575
|
return 'close-unsupported';
|
|
@@ -641,6 +750,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
641
750
|
];
|
|
642
751
|
|
|
643
752
|
const closeOpts = { timeoutMs: closeTimeoutMs, cwd: workdir };
|
|
753
|
+
/** D8 — the DECLARED close's seed, filled in below before anything runs.
|
|
754
|
+
* `null` for a command close, which has no baseline to measure against. */
|
|
755
|
+
/** @type {{seedRef: string}|null} */
|
|
756
|
+
let declaredCtx = null;
|
|
757
|
+
/**
|
|
758
|
+
* ONE close-execution seam, chosen once by which field the SIGNED spec carries.
|
|
759
|
+
* Both executors return `runClose`'s verdict shape, so everything downstream —
|
|
760
|
+
* the precheck, the preflight, the check seam, ralph's judge, `CLOSE_FAULTS`,
|
|
761
|
+
* the trend reader and Layer R — is the same code for both.
|
|
762
|
+
*
|
|
763
|
+
* The command path is byte-identical to what it was: same call, same options.
|
|
764
|
+
* @param {any[]} stages
|
|
765
|
+
*/
|
|
766
|
+
const runCloseStages = (stages) => (declared
|
|
767
|
+
? runDeclaredStages(stages, scrub, { ...closeOpts, seedRef: /** @type {any} */ (declaredCtx).seedRef })
|
|
768
|
+
: runStages(stages, scrub, closeOpts));
|
|
644
769
|
/** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
|
|
645
770
|
let closeStage;
|
|
646
771
|
/** @type {any} the LAST close verdict itself (W-2). A wall stop KEEPS the grade
|
|
@@ -698,16 +823,172 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
698
823
|
};
|
|
699
824
|
/** the close, as ONE verdict: every stage in order, first red wins */
|
|
700
825
|
const judgeClose = async () => {
|
|
701
|
-
const v = await
|
|
826
|
+
const v = await runCloseStages(stagedClose);
|
|
702
827
|
closeStage = v.stage;
|
|
703
828
|
lastCloseVerdict = v;
|
|
704
829
|
// Only a RED grade is a reading. A `satisfied` ends the run and a close FAULT
|
|
705
830
|
// rendered no judgment at all (CLOSE_FAULTS) — folding either into the series
|
|
706
831
|
// would put a non-number where the instrument expects a graded one.
|
|
707
|
-
|
|
832
|
+
// `closeGrade` is the reader's input for EITHER executor: a command close
|
|
833
|
+
// hands over its gap and `readGrade` scrapes the stage and the count out of
|
|
834
|
+
// it; a declared close already KNOWS both and says them, rather than
|
|
835
|
+
// round-tripping a number through prose it would then have to parse back.
|
|
836
|
+
if (v.verdict === 'needs_revision') runTrend.record(closeGrade(v));
|
|
708
837
|
return v;
|
|
709
838
|
};
|
|
710
839
|
|
|
840
|
+
// ── 0c. THE WORK BRANCH — the HARD RULE (PRD Addendum v1.57 §3, hamr):
|
|
841
|
+
// *"The agent creates a NEW BRANCH before it touches any code — a HARD RULE, no
|
|
842
|
+
// exceptions. Not a default, not a preference: no job edits the branch it was
|
|
843
|
+
// handed, and none edits `main`."*
|
|
844
|
+
//
|
|
845
|
+
// ONE SEAM, TWO PLACEMENTS. The split is COLD vs RESUME, and every half of it is
|
|
846
|
+
// ruled rather than convenient:
|
|
847
|
+
//
|
|
848
|
+
// * BEFORE the scout on BOTH paths, the scout being the run's first PAID call. A
|
|
849
|
+
// patient that is not a git checkout, a namespace with no free name, a resume
|
|
850
|
+
// whose branch a human deleted — each is an instrument stop that must cost zero
|
|
851
|
+
// tokens, and a branch prepared after the scout would already have bought one.
|
|
852
|
+
// * COLD: AFTER the precheck and the preflight, which are $0 and deterministic.
|
|
853
|
+
// An ALREADY-GREEN tree returns below without ever reaching the seam and leaves
|
|
854
|
+
// no branch behind: a run with no work to do has no blast radius to bound, and
|
|
855
|
+
// minting a branch for it would litter every patient with the record of a run
|
|
856
|
+
// that did nothing.
|
|
857
|
+
// * RESUME: BEFORE them — here, above 0*. There is nothing to mint on a resume
|
|
858
|
+
// (the recorded branch exists, `created:false`, and the arm below refuses
|
|
859
|
+
// rather than creating), so the leave-no-branch-behind clause has nothing to
|
|
860
|
+
// say; what IS at stake is WHICH TREE the $0 instruments measure. The recorded
|
|
861
|
+
// branch holds the work the operator already paid for and IS the run being
|
|
862
|
+
// continued, while the ref the operator handed back can be anything at all. Run
|
|
863
|
+
// from where the run happens to stand, the precheck could read `already-green`
|
|
864
|
+
// off a tree that is not this run's and every `baseline: "seed"` stage would
|
|
865
|
+
// baseline against it — an instrument measuring the wrong subject, reading
|
|
866
|
+
// honestly and saying nothing true. Moving the seam up is also strictly cheaper
|
|
867
|
+
// for the failure case: a resume whose branch is gone or foreign now stops
|
|
868
|
+
// before the arbiter grades anything at all.
|
|
869
|
+
//
|
|
870
|
+
// The scout itself needs no branch and is not what this gates: its menu simply
|
|
871
|
+
// has no write-class verb in it (the menu IS the grant), so it cannot touch code
|
|
872
|
+
// wherever it stands. What the branch gates is the WRITE-CAPABLE phase, and that
|
|
873
|
+
// gate is enforced structurally rather than by this ordering — `mkWorker` refuses
|
|
874
|
+
// to build a `writable` worker while `workBranch` is null (below). Ordering says
|
|
875
|
+
// where the honest stop happens; the guard says the rule cannot be bypassed.
|
|
876
|
+
//
|
|
877
|
+
// Creating a branch at HEAD moves no commit and changes nothing the close can
|
|
878
|
+
// measure: the declared seed is HEAD either way, and the tree — dirty or clean —
|
|
879
|
+
// is carried across untouched.
|
|
880
|
+
/** @type {string|null} the branch this run's work lands on, once it exists */
|
|
881
|
+
let workBranch = null;
|
|
882
|
+
/** is this leg continuing work that already exists? Spelled EXACTLY as
|
|
883
|
+
* `prepareWorkBranch`'s own resume test, so the two cannot disagree about which
|
|
884
|
+
* arm a given input takes. */
|
|
885
|
+
const resuming = resumeBranch !== null && resumeBranch !== undefined;
|
|
886
|
+
/**
|
|
887
|
+
* 0c itself — ONE definition for both placements. The refusal below is written
|
|
888
|
+
* once on purpose: two call sites emitting their own branch-red would be two
|
|
889
|
+
* messages free to drift apart, and this one is what a human reads when a run
|
|
890
|
+
* stops having spent nothing.
|
|
891
|
+
* @returns {Promise<'branch-red'|null>} the terminal to return, or `null` once the
|
|
892
|
+
* run is standing on a branch nobody handed it
|
|
893
|
+
*/
|
|
894
|
+
const prepareBranch = async () => {
|
|
895
|
+
const wb = await prepareWorkBranch(workdir, { name: workBranchName(job), resume: resumeBranch ?? null });
|
|
896
|
+
if (wb.stop !== null) {
|
|
897
|
+
// Never a fallback to the handed branch. That fallback IS the thing the rule
|
|
898
|
+
// forbids, and a run that silently took it would report a green whose blast
|
|
899
|
+
// radius nobody bounded — so this is a named terminal of its own rather than
|
|
900
|
+
// a wiring fault (`interpreter-red` aims an upstream ask at a library; a
|
|
901
|
+
// patient that is not a git checkout is the operator's own state speaking).
|
|
902
|
+
emit('escalation', {
|
|
903
|
+
category: 'branch-red', decisionReady: true,
|
|
904
|
+
decision: 'The run could not create its own work branch, and no job runs on the branch it was handed (PRD v1.57 §3). Nothing was spent.',
|
|
905
|
+
options: [
|
|
906
|
+
'make the patient a git checkout with at least one commit',
|
|
907
|
+
'free the work-branch name (delete or rename the stale branches this run collided with)',
|
|
908
|
+
'abandon the run',
|
|
909
|
+
],
|
|
910
|
+
detail: scrub(wb.stop),
|
|
911
|
+
});
|
|
912
|
+
return 'branch-red';
|
|
913
|
+
}
|
|
914
|
+
workBranch = wb.branch;
|
|
915
|
+
// The run's own books say where its work went. A human reading a spine — or a
|
|
916
|
+
// resume reading it back (`readResume`'s `restart.branch`) — must not have to
|
|
917
|
+
// re-derive the name from the spec and guess how many collisions there were.
|
|
918
|
+
emit('work-branch', {
|
|
919
|
+
branch: wb.branch,
|
|
920
|
+
created: wb.created,
|
|
921
|
+
resumed: wb.resumed,
|
|
922
|
+
from: wb.from,
|
|
923
|
+
base: wb.base,
|
|
924
|
+
repo: wb.repo,
|
|
925
|
+
...(wb.collided > 0 ? { collided: wb.collided } : {}),
|
|
926
|
+
meaning: wb.resumed
|
|
927
|
+
? 'returned to the branch this run was killed on — its work is on this branch, and a fresh one would strand it'
|
|
928
|
+
: 'the HARD RULE (PRD v1.57 §3): the run works here, never on the branch it was handed and never on main',
|
|
929
|
+
});
|
|
930
|
+
return null;
|
|
931
|
+
};
|
|
932
|
+
if (resuming) {
|
|
933
|
+
const stop = await prepareBranch();
|
|
934
|
+
if (stop !== null) return stop;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ── 0*. the DECLARED close's two run-start facts. Both are $0 and both sit
|
|
938
|
+
// INSIDE the clock (they are the arbiter's own instruments, and time the
|
|
939
|
+
// operator's instruments too or the budget measures a different run).
|
|
940
|
+
//
|
|
941
|
+
// (1) THE SEED (D8/D12). HEAD at run start, READ and recorded, never typed.
|
|
942
|
+
// The close stores the counting RULE and never the number, so every
|
|
943
|
+
// `baseline: "seed"` stage is measured against THIS run's own starting
|
|
944
|
+
// point — a constant frozen at signing would judge run 5 against run 1's
|
|
945
|
+
// tree, which is the one-experiment shape D12 retires.
|
|
946
|
+
//
|
|
947
|
+
// (2) THE GROUNDED RE-VALIDATION (D9 gate 1). The job validator judged the
|
|
948
|
+
// declaration with no repository in hand, so hamr's listing rule and the
|
|
949
|
+
// scoped-job derivation that arms the F84 one-population law were
|
|
950
|
+
// DEFERRED, not skipped. They run here, against the real seed tree,
|
|
951
|
+
// before any stage and before any token. A declaration that names a path
|
|
952
|
+
// the tree does not have is a broken close, not a red about the worker —
|
|
953
|
+
// round 2's arm A invented `src/alertEmail.js` and silently reclassified
|
|
954
|
+
// 15 real errors into the wrong population.
|
|
955
|
+
if (declared) {
|
|
956
|
+
const s = await seedAtHead(workdir);
|
|
957
|
+
if (s.stop !== null) {
|
|
958
|
+
emit('escalation', {
|
|
959
|
+
category: CLOSE_FAULTS.failed.category, decisionReady: true,
|
|
960
|
+
decision: `The authored close measures against this run's own seed, and the seed could not be read. ${CLOSE_FAULTS.failed.decision}`,
|
|
961
|
+
options: ['check the repository is a git checkout with at least one commit', ...CLOSE_FAULTS.failed.options],
|
|
962
|
+
detail: scrub(s.stop),
|
|
963
|
+
});
|
|
964
|
+
return 'close-red';
|
|
965
|
+
}
|
|
966
|
+
const listed = await seedListing(workdir, s.seedRef);
|
|
967
|
+
const v = listed.stop === null
|
|
968
|
+
? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType })
|
|
969
|
+
: { ok: false, grounded: false, reds: [{ code: 'listing-unreadable', path: 'closeDecl', detail: listed.stop }] };
|
|
970
|
+
emit('close-decl', {
|
|
971
|
+
genre: job.closeDecl.genre,
|
|
972
|
+
lang: job.closeDecl.lang,
|
|
973
|
+
seedRef: s.seedRef,
|
|
974
|
+
stages: stagedClose.map((/** @type {any} */ st) => st.name),
|
|
975
|
+
grounded: v.grounded,
|
|
976
|
+
ok: v.ok,
|
|
977
|
+
...(v.ok ? {} : { reds: v.reds.map((/** @type {any} */ r) => ({ ...r, detail: scrub(String(r.detail ?? '')) })) }),
|
|
978
|
+
});
|
|
979
|
+
if (!v.ok) {
|
|
980
|
+
emit('escalation', {
|
|
981
|
+
category: CLOSE_FAULTS.failed.category, decisionReady: true,
|
|
982
|
+
decision: 'The authored close does not validate against the repository it is about to judge — a path it names, '
|
|
983
|
+
+ 'or a population it counts, does not match the seed tree. Nothing it rendered would be trustworthy.',
|
|
984
|
+
options: ['re-author the close against this repository (a new declaration is a new spec hash, and needs re-signing)', ...CLOSE_FAULTS.failed.options],
|
|
985
|
+
detail: v.reds.map((/** @type {any} */ r) => `${r.code}:${r.path}${r.detail ? ` — ${scrub(String(r.detail))}` : ''}`).join('\n'),
|
|
986
|
+
});
|
|
987
|
+
return 'close-red';
|
|
988
|
+
}
|
|
989
|
+
declaredCtx = { seedRef: s.seedRef };
|
|
990
|
+
}
|
|
991
|
+
|
|
711
992
|
// ── 0a. close precheck (close-first, F17): already-green is a DISTINCT
|
|
712
993
|
// record, zero tokens; a forbidden-zone verdict escalates before any spend
|
|
713
994
|
const pre = await judgeClose();
|
|
@@ -741,8 +1022,26 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
741
1022
|
: {}),
|
|
742
1023
|
});
|
|
743
1024
|
for (const m of menu) {
|
|
744
|
-
const v = await
|
|
745
|
-
if (v.verdict === 'needs_revision')
|
|
1025
|
+
const v = await runCloseStages(m.run);
|
|
1026
|
+
if (v.verdict === 'needs_revision') {
|
|
1027
|
+
seedRed.push(m.name);
|
|
1028
|
+
// …and the preflight grade is a READING, for the same reason the precheck one
|
|
1029
|
+
// above is: it is the seed the run's work is measured against. The precheck
|
|
1030
|
+
// only ever reds at the close's FIRST failing stage (first-red-wins), so on
|
|
1031
|
+
// every shipped close — each of which opens with `changed-from-seed` — the
|
|
1032
|
+
// numeric stages have no baseline at all without it. A step's first grade
|
|
1033
|
+
// would then have nothing to compare against and the whole run would read
|
|
1034
|
+
// `unknown` on exactly the stop the readout exists for.
|
|
1035
|
+
//
|
|
1036
|
+
// ONCE PER STAGE, though. The precheck and this loop grade the SAME unchanged
|
|
1037
|
+
// tree back to back with no work in between, so a stage both reach reports the
|
|
1038
|
+
// same number twice — and a repeated baseline in a series reads as an attempt
|
|
1039
|
+
// that achieved nothing (`verdict 2 → 2 → 1`). That is a phantom flat step
|
|
1040
|
+
// handed to a human and to the replanner, which is the exact class of false
|
|
1041
|
+
// story this change exists to remove. Asked of the reader's own books, so
|
|
1042
|
+
// there is no second record of what has been graded.
|
|
1043
|
+
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record(closeGrade(v));
|
|
1044
|
+
}
|
|
746
1045
|
emit('check-preflight', { name: m.name, verdict: v.verdict, ...(m.run.length > 1 ? { chain: m.run.map((s2) => s2.name) } : {}) });
|
|
747
1046
|
const f = Object.hasOwn(CLOSE_FAULTS, v.verdict) ? CLOSE_FAULTS[v.verdict] : undefined;
|
|
748
1047
|
if (f) {
|
|
@@ -751,6 +1050,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
751
1050
|
}
|
|
752
1051
|
}
|
|
753
1052
|
|
|
1053
|
+
// ── 0c, COLD. The seam and both halves of its placement ruling are stated
|
|
1054
|
+
// above, where a RESUME reaches it; this is where a run that is NOT resuming
|
|
1055
|
+
// does — after the $0 precheck and preflight, so an already-green tree has
|
|
1056
|
+
// already returned and left no branch behind.
|
|
1057
|
+
if (!resuming) {
|
|
1058
|
+
const stop = await prepareBranch();
|
|
1059
|
+
if (stop !== null) return stop;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
754
1062
|
const lc = new LiteCtx({ root: workdir });
|
|
755
1063
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
756
1064
|
const fencePrefixes = job.writeScope.map((/** @type {string} */ g) => resolve(workdir, globToPrefix(g)));
|
|
@@ -794,10 +1102,27 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
794
1102
|
// derivation, not a live path; the reachable guard is the validator's, and
|
|
795
1103
|
// that one is mutation-covered.
|
|
796
1104
|
if (!chain) return { pass: false, fault: 'failed', gap: `no offered close stage named "${name}"` };
|
|
797
|
-
const v = await
|
|
1105
|
+
const v = await runCloseStages(chain);
|
|
798
1106
|
emit('check-run', { name, verdict: v.verdict, ...(v.stage && v.stage !== name ? { stage: v.stage } : {}), ...(v.exitCode !== undefined ? { exitCode: v.exitCode } : {}) });
|
|
799
1107
|
if (v.verdict === 'satisfied') return { pass: true };
|
|
800
|
-
if (v.verdict === 'needs_revision')
|
|
1108
|
+
if (v.verdict === 'needs_revision') {
|
|
1109
|
+
// The run trend is fed by EVERY grade the arbiter renders, and a check IS an
|
|
1110
|
+
// arbiter-rendered close grade: the same `runStages`, the same chain, the same
|
|
1111
|
+
// stage names, so it lands in the same per-stage buckets and the accuracy law
|
|
1112
|
+
// holds unchanged. Without this the reader is blind to everything a STEP ever
|
|
1113
|
+
// achieves — it sees only the precheck and the outer fix loop — which is why
|
|
1114
|
+
// u-msh70zla's meter, firing mid-step, had nothing to report about a step that
|
|
1115
|
+
// had just gone 24 → 15 → 14.
|
|
1116
|
+
//
|
|
1117
|
+
// Folded HERE, on `v.gap`, and the seam is load-bearing: `evalExits` wraps this
|
|
1118
|
+
// gap as `check "<name>" red: …`, and THAT line carries the word "red" with no
|
|
1119
|
+
// number on it, so `readGrade` reads the wrapper instead of the wall and every
|
|
1120
|
+
// step grade donates a null. Measured on u-msh70zla's own archived gaps: raw
|
|
1121
|
+
// reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Same seam the
|
|
1122
|
+
// Layer R note below already calls out for a check's `^`-anchored gapKeep.
|
|
1123
|
+
runTrend.record(closeGrade(v));
|
|
1124
|
+
return { pass: false, gap: v.gap };
|
|
1125
|
+
}
|
|
801
1126
|
return { pass: false, fault: v.verdict, gap: v.detail };
|
|
802
1127
|
};
|
|
803
1128
|
|
|
@@ -823,6 +1148,27 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
823
1148
|
* the turn-allowance note below.
|
|
824
1149
|
*/
|
|
825
1150
|
async function mkWorker({ granted, phase, attemptRounds, attempts, writable, root = null, fence = null, workerProvider = null }) {
|
|
1151
|
+
// THE HARD RULE, ENFORCED STRUCTURALLY (PRD v1.57 §3). This is the ONE seam
|
|
1152
|
+
// that grants write-class verbs — `writeScope` is empty for every other
|
|
1153
|
+
// worker, so a read-only phase cannot reach a file whatever it intends — and
|
|
1154
|
+
// a branch is therefore a PRECONDITION of it, not a courtesy the ordering
|
|
1155
|
+
// above happens to provide. "No exceptions" has to mean the rule survives a
|
|
1156
|
+
// future caller who adds a third writable worker and forgets the ordering.
|
|
1157
|
+
//
|
|
1158
|
+
// Unreachable through `runPlan` as it stands (0c runs before the scout and
|
|
1159
|
+
// returns `branch-red` on any fault), which is exactly what a backstop should
|
|
1160
|
+
// be. Categorised so it lands as the wiring fault it is: the step site and
|
|
1161
|
+
// `relay` both return `interpreter-red` verbatim, so the escalation a human
|
|
1162
|
+
// reads and the outcome the spine records name the same thing.
|
|
1163
|
+
if (writable && workBranch === null) {
|
|
1164
|
+
const err = /** @type {any} */ (new Error(
|
|
1165
|
+
`HARD RULE (PRD v1.57 §3): a write-capable worker was requested for phase "${phase}" with no work branch prepared — `
|
|
1166
|
+
+ 'no job edits the branch it was handed. The run is refused rather than allowed to write where it stands.',
|
|
1167
|
+
));
|
|
1168
|
+
err.category = 'interpreter-red';
|
|
1169
|
+
err.lib = 'bareloop';
|
|
1170
|
+
throw err;
|
|
1171
|
+
}
|
|
826
1172
|
/**
|
|
827
1173
|
* The gate's LLM-turn allowance THROUGH iteration `i` — the old expression
|
|
828
1174
|
* `attemptRounds * (attempts + 1)` with the iteration count made explicit
|
|
@@ -1256,6 +1602,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1256
1602
|
// and those are different diagnoses with different remedies (F45/F48: a
|
|
1257
1603
|
// casualty is never evidence, and a governance stop is never a casualty).
|
|
1258
1604
|
'step-stalled': [`The model stopped producing rounds during ${phase} and reissuing the call did not recover it.`, ['retry the run', 'check provider status', 'abandon the run']],
|
|
1605
|
+
// A WIRING/INVARIANT fault is not a transport casualty, and the default
|
|
1606
|
+
// below would file it as one. The step loop already restores this category
|
|
1607
|
+
// verbatim at its own setup site ("the RECORDED outcome must match the
|
|
1608
|
+
// escalation the human reads", review #6/F11); the fix loop reached `relay`
|
|
1609
|
+
// instead and every such fault came back `provider-red` — a broken runner
|
|
1610
|
+
// reported as the provider's fault, and an upstream ask aimed at a library
|
|
1611
|
+
// for our own missing wiring (the step-stalled lesson).
|
|
1612
|
+
'interpreter-red': [`The ${phase} phase could not be set up — the runner or a primitive it was handed is not correctly bound.`, ['fix the wiring the detail names', 'retry the run', 'abandon the run']],
|
|
1259
1613
|
};
|
|
1260
1614
|
const [decision, options] = (Object.hasOwn(DECIDE, category) ? DECIDE[category] : undefined)
|
|
1261
1615
|
?? [`The ${phase} call failed (${category}) — no result exists.`, ['retry the run', 'fix the provider binding', 'abandon the run']];
|
|
@@ -1266,7 +1620,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1266
1620
|
// `step-stalled` rides out as ITSELF, not as provider-red. Naming the
|
|
1267
1621
|
// escalation while returning a casualty label would launder a governance stop
|
|
1268
1622
|
// into transport noise at exactly the layer the readout reads.
|
|
1269
|
-
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled'
|
|
1623
|
+
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red'
|
|
1624
|
+
? category : 'provider-red';
|
|
1270
1625
|
};
|
|
1271
1626
|
|
|
1272
1627
|
// ── 1. SCOUT — read-only by construction: the write-class verbs are simply
|
|
@@ -1482,8 +1837,39 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1482
1837
|
const artifacts = [];
|
|
1483
1838
|
/** @type {{id: string, outcome: string}[]} */
|
|
1484
1839
|
const stepOutcomes = [];
|
|
1485
|
-
|
|
1486
|
-
|
|
1840
|
+
/** RESUME — the replan ledger the CHAIN has already spent, belted the way every
|
|
1841
|
+
* other declared fold entering the arbiter's arithmetic is (`priorSpentUsd`,
|
|
1842
|
+
* `priorWallMs`, `declaredRounds`): a garbage or negative seed reads as 0 rather
|
|
1843
|
+
* than as a NaN that would poison every later comparison, and a negative one must
|
|
1844
|
+
* never cancel a real replan and widen the ceiling (reuse's `> 0` belt, F-4). */
|
|
1845
|
+
const seededReplans = typeof resumeReplans?.count === 'number' && Number.isFinite(resumeReplans.count) && resumeReplans.count > 0
|
|
1846
|
+
? Math.floor(resumeReplans.count) : 0;
|
|
1847
|
+
// Seeded, not reset. These three were locals reborn on every `runPlan` call — and a
|
|
1848
|
+
// resume IS another call, so the ceiling was refilled by every kill (see the
|
|
1849
|
+
// `resumeReplans` param doc for the measurement and for why this bound spans the
|
|
1850
|
+
// chain while the trend's iteration bound deliberately does not).
|
|
1851
|
+
let replanned = seededReplans > 0;
|
|
1852
|
+
/** F85 (C): how many replans this run has actually drafted, and whether the
|
|
1853
|
+
* arbiter has already spent its ONE extra grant. Two variables and not one
|
|
1854
|
+
* counter compared against a limit, deliberately — a latch cannot be widened by
|
|
1855
|
+
* arithmetic, and the ceiling is the thing this rung must not move (v1.12). */
|
|
1856
|
+
let replans = seededReplans;
|
|
1857
|
+
// the F85-C latch travels BESIDE the count and is not derived from it: a leg can
|
|
1858
|
+
// inherit a spent ordinary ceiling with the grant still unearned (count 1, false),
|
|
1859
|
+
// and deriving one from the other would collapse those two states into one.
|
|
1860
|
+
let varianceGrantUsed = resumeReplans?.grantUsed === true;
|
|
1861
|
+
// `replanned` stays the shipped boolean this record has always carried; `replans`
|
|
1862
|
+
// rides BESIDE it (append-only — a field's meaning is never repurposed).
|
|
1863
|
+
//
|
|
1864
|
+
// Both now count the CHAIN rather than the leg, and that IS a meaning change — made
|
|
1865
|
+
// rather than shadowed by a second field because `replans` shipped only on this
|
|
1866
|
+
// unreleased branch, so no archived spine carries the leg reading for a reader to
|
|
1867
|
+
// be confused by. The leg's own number is not lost by it: a leg emits one `replan`
|
|
1868
|
+
// record per replan it drafts, so `chain − records-in-this-window` is the fold it
|
|
1869
|
+
// inherited, and that is exactly the arithmetic `readResume` runs to rebuild the
|
|
1870
|
+
// ledger for the next leg. A second field stating a number already derivable from
|
|
1871
|
+
// the window would be a second reader of one question (F6's structural cousin).
|
|
1872
|
+
const planExecuted = () => emit('plan-executed', { steps: stepOutcomes, replanned, replans });
|
|
1487
1873
|
/** @type {object|null} W-2 at the STEP site — set when the WALL refused to open a
|
|
1488
1874
|
* step's attempt. It carries that stop's own record fields, and its presence is what
|
|
1489
1875
|
* lets the step loop's terminal tell this reading apart from F64's mid-call one
|
|
@@ -1558,15 +1944,41 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1558
1944
|
const moneyShare = stepStartUsd > 0 ? (stepStartUsd - remainingUsd()) / stepStartUsd : 0;
|
|
1559
1945
|
const timeShare = clock.bounded && stepStartMs > 0 ? (stepStartMs - clock.remainingMs()) / stepStartMs : 0;
|
|
1560
1946
|
if (moneyShare >= VARIANCE_THRESHOLD || timeShare >= VARIANCE_THRESHOLD) {
|
|
1947
|
+
// F85 — the meter REPORTS progress; it does not decide on it. The
|
|
1948
|
+
// condition above is the whole trigger and stays the whole trigger: a
|
|
1949
|
+
// converging step that is eating the run is still stopped, because this is
|
|
1950
|
+
// a governance instrument over an operator-owned allowance and a governor
|
|
1951
|
+
// that suppressed itself whenever the work looked promising would be
|
|
1952
|
+
// judging capability with the budget. What was missing was never a term in
|
|
1953
|
+
// the condition — it was the READING. u-msh70zla stopped a step whose close
|
|
1954
|
+
// had gone 24 → 15 → 14 and then told the replanner its exits were
|
|
1955
|
+
// "unmoved", so the replanner threw the progress away and re-targeted a
|
|
1956
|
+
// file that was already clean.
|
|
1957
|
+
//
|
|
1958
|
+
// The reading comes off `runTrend` — the SAME instance the money halt reads
|
|
1959
|
+
// (src/trend.js's ONE PER SERIES). A second reader for one question is how
|
|
1960
|
+
// two readers come to disagree, which is precisely the defect being fixed.
|
|
1961
|
+
// It is per-stage, numbers-and-stage-names only, and `unknown` stays
|
|
1962
|
+
// `unknown` — a close that reports no number donates nothing (F6).
|
|
1963
|
+
const t = runTrend.verdict();
|
|
1561
1964
|
emit('variance', {
|
|
1562
1965
|
step: step.id, iteration, threshold: VARIANCE_THRESHOLD,
|
|
1563
1966
|
moneyShare: Number(moneyShare.toFixed(3)),
|
|
1564
1967
|
timeShare: clock.bounded ? Number(timeShare.toFixed(3)) : null,
|
|
1565
1968
|
axis: moneyShare >= VARIANCE_THRESHOLD ? 'money' : 'time',
|
|
1969
|
+
// ADDED fields, never repurposed ones (the spine is append-only forever).
|
|
1970
|
+
// Same four names and same shapes the money halt already emits, so one
|
|
1971
|
+
// reader parses a progress reading wherever it appears.
|
|
1972
|
+
trend: t.trend, motion: t.motion, reading: t.reading, series: t.series,
|
|
1566
1973
|
});
|
|
1567
1974
|
const err = /** @type {CategorizedError} */ (new Error(
|
|
1568
1975
|
`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`
|
|
1976
|
+
+ `${moneyShare >= VARIANCE_THRESHOLD ? 'money' : 'time'} across ${iteration - 1} attempt(s) with its exits still red. `
|
|
1977
|
+
// The escalation `detail` is this message (ralph passes it through), and
|
|
1978
|
+
// the replan brief quotes the detail — so this sentence is what a human
|
|
1979
|
+
// AND the redrafting planner read. It states what the run achieved; it
|
|
1980
|
+
// never states what to do about it, which is the operator's lever list.
|
|
1981
|
+
+ `Run progress: ${t.reading}.`));
|
|
1570
1982
|
err.category = 'step-variance';
|
|
1571
1983
|
throw err;
|
|
1572
1984
|
}
|
|
@@ -1772,12 +2184,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1772
2184
|
// pricing-red at the step-end guard above, so isUnpriced() is false here.)
|
|
1773
2185
|
//
|
|
1774
2186
|
// 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).
|
|
2187
|
+
// step that had eaten a declared share of the run. That is the case
|
|
2188
|
+
// exhaustion-only could never reach (F56: a replan has fired zero times in the
|
|
2189
|
+
// programme), and it is a genuine re-allocation rather than a stop. Both
|
|
2190
|
+
// triggers still need FUNDS LEFT, and both are bounded by the replan ceiling.
|
|
1780
2191
|
const cat = lastEscalation?.category;
|
|
2192
|
+
/** F85 — the run's measured trajectory at the moment of this stop, read ONCE
|
|
2193
|
+
* here off the same instance the meter and the money halt read. Both consumers
|
|
2194
|
+
* below are the replanner's: the trigger sentence and the brief. */
|
|
2195
|
+
const stopTrend = runTrend.verdict();
|
|
1781
2196
|
// F66 adds the THIRD trigger: `step-stalled` — the stall watchdog reissued the
|
|
1782
2197
|
// model call MAX_STALLS times and never got a round back. hamr's ruling is that
|
|
1783
2198
|
// a stall must self-heal rather than end the run ("killing and coming back is
|
|
@@ -1785,13 +2200,58 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1785
2200
|
// demonstrably stopped working does the run adapt instead — by replanning, not
|
|
1786
2201
|
// by stopping. Same ONE-replan ceiling and same FUNDS-LEFT condition as the
|
|
1787
2202
|
// other two: this changes the trigger, never the ceiling (v1.12).
|
|
2203
|
+
//
|
|
2204
|
+
// F85 rewrote the variance branch. It used to be the FIXED string "…with its
|
|
2205
|
+
// exits unmoved", printed for every step-variance stop whatever had happened —
|
|
2206
|
+
// and on u-msh70zla it was simply false: the exits had moved 24 → 15 → 14. The
|
|
2207
|
+
// sentence now states the meter's fact (a share of the run) and then the trend
|
|
2208
|
+
// instrument's MEASURED reading, which is the same reading the `variance` record
|
|
2209
|
+
// and the money halt carry. Nothing here asserts a direction the reader did not
|
|
2210
|
+
// report: `unknown` says unknown.
|
|
1788
2211
|
const replanTrigger = cat === 'cap-halt' ? 'step exhausted its attempts with exits still red'
|
|
1789
|
-
: cat === 'step-variance' ?
|
|
2212
|
+
: cat === 'step-variance' ? `the meter stopped a step that was consuming the run — ${stopTrend.reading}`
|
|
1790
2213
|
: cat === 'step-stalled' ? 'the model stopped producing rounds and reissuing the call did not recover it'
|
|
1791
2214
|
: null;
|
|
1792
|
-
|
|
2215
|
+
// F85 (C) — the ARBITER's one additional replan. A second `step-variance` after
|
|
2216
|
+
// the ceiling is spent used to be a hard stop, and on a run that was still
|
|
2217
|
+
// converging that stop threw away work the run had already paid for. So the
|
|
2218
|
+
// arbiter may grant ONE more, and every clause of that sentence is a constraint:
|
|
2219
|
+
//
|
|
2220
|
+
// * ONE. Bounded, and bounded by a latch rather than a comparison, so the
|
|
2221
|
+
// ceiling cannot creep: unlimited replanning launders thrash as adaptation
|
|
2222
|
+
// (v1.12). A third variance stop is the stop, however well it is going.
|
|
2223
|
+
// * the ARBITER. Read mechanically off `runTrend` — the same instance the
|
|
2224
|
+
// meter and the money halt read. The agent never asks for it, never sees it
|
|
2225
|
+
// offered, and cannot influence it: no self-adjusted budgets, ever.
|
|
2226
|
+
// * `converging`, the trend instrument's OWN category. No new number is
|
|
2227
|
+
// invented here — threshold-setting is the operator's, and a fresh
|
|
2228
|
+
// comparison would be a second reader of the one question (src/trend.js).
|
|
2229
|
+
// `flat` and `unknown` are refusals; an unknown is never rounded up into a
|
|
2230
|
+
// reason to spend (F6).
|
|
2231
|
+
// * step-variance only. An exhaustion or a stall after the ceiling is
|
|
2232
|
+
// unchanged — this widens nothing but the case it was measured on.
|
|
2233
|
+
const grantExtraReplan = replanned
|
|
2234
|
+
&& cat === 'step-variance'
|
|
2235
|
+
&& !varianceGrantUsed
|
|
2236
|
+
&& stopTrend.trend === 'converging';
|
|
2237
|
+
if ((!replanned || grantExtraReplan) && replanTrigger && remainingUsd() > MONEY_MIN) {
|
|
2238
|
+
if (grantExtraReplan) varianceGrantUsed = true;
|
|
1793
2239
|
replanned = true;
|
|
1794
|
-
|
|
2240
|
+
replans += 1;
|
|
2241
|
+
emit('replan', {
|
|
2242
|
+
step: step.id, reason: replanTrigger, trigger: cat,
|
|
2243
|
+
// ADDED fields (append-only): which replan this is, and — only when the
|
|
2244
|
+
// arbiter granted one past the ordinary ceiling — the reading it granted it
|
|
2245
|
+
// on. Absent on the ordinary replan, so a reader can never mistake the
|
|
2246
|
+
// ceiling for a grant.
|
|
2247
|
+
//
|
|
2248
|
+
// The index is the RUN's, so on a resumed leg it opens above 1 — the ceiling
|
|
2249
|
+
// it is counted against spans the chain, and an index that restarted at 1 on
|
|
2250
|
+
// every leg would read as a fresh allowance on the one record that could have
|
|
2251
|
+
// shown it was not.
|
|
2252
|
+
replan: replans,
|
|
2253
|
+
...(grantExtraReplan ? { granted: stopTrend.trend } : {}),
|
|
2254
|
+
});
|
|
1795
2255
|
// The brief names the trigger HONESTLY, one branch per trigger: it is the
|
|
1796
2256
|
// only channel the redrafting planner adapts to, so a wrong diagnosis here
|
|
1797
2257
|
// is handed to the one component whose whole job is to respond to it. A
|
|
@@ -1809,28 +2269,45 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1809
2269
|
const leftOver = `$${remainingUsd().toFixed(2)} and `
|
|
1810
2270
|
+ `${clock.bounded ? `${Math.round(clock.remainingMs() / 60_000)} minute(s)` : 'time UNBOUNDED'} `
|
|
1811
2271
|
+ 'of the run were still unspent when it stopped.';
|
|
2272
|
+
// F85: the meter's branch states the METER's fact and then the run's measured
|
|
2273
|
+
// one, in that order and as two separate claims — "it was stopped for eating
|
|
2274
|
+
// the run" and "here is what the run achieved" are different things, and the
|
|
2275
|
+
// old sentence quietly fused them into a verdict on the work.
|
|
1812
2276
|
const why = cat === 'step-variance'
|
|
1813
|
-
? 'It was stopped by the run\'s meter for consuming too large a share of what was left.'
|
|
2277
|
+
? 'It was stopped by the run\'s meter for consuming too large a share of what was left. '
|
|
2278
|
+
+ `Its work so far, from the close's own numbers: ${stopTrend.reading}. ${leftOver}`
|
|
1814
2279
|
: cat === 'step-stalled'
|
|
1815
2280
|
? 'It stalled: the model stopped producing rounds and reissuing the call did not recover it, so its exits were never judged.'
|
|
1816
2281
|
: `${res.ladder.brief()} ${leftOver}`;
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2282
|
+
// F86 — this step's OWN last exit results, verbatim (u-mshcpdg4). Everything
|
|
2283
|
+
// above this line is the run's narration of the stop: the meter's share
|
|
2284
|
+
// sentence, the trend's direction, the never-wrote fact, the escalation detail.
|
|
2285
|
+
// None of them says WHERE the remaining work is, because none of them is the
|
|
2286
|
+
// close. The close's output is in HERE — as one of the failing exits' details,
|
|
2287
|
+
// beside the evaluator's own (a step that wrote nothing leads with
|
|
2288
|
+
// `tree-changed`), which is why the block is labelled by its exits rather than
|
|
2289
|
+
// by the close. It goes over as text — see `closeGapBlock` for why a parsed
|
|
2290
|
+
// file list would reproduce the exact bug it is fixing. Empty gap → empty
|
|
2291
|
+
// string → the brief is byte-identical to the pre-F86 one.
|
|
2292
|
+
const gapBlock = closeGapBlock(res?.gap);
|
|
1824
2293
|
const failure = `Step "${step.id}" (${step.action}) did not reach its exits. `
|
|
1825
2294
|
+ `${why}\n`
|
|
1826
|
-
+ missedLine
|
|
1827
2295
|
+ `Last exit state:\n${lastEscalation?.detail ?? '(none)'}\n`
|
|
1828
|
-
+ `Steps completed so far: ${artifacts.map((a) => a.id).join(', ') || 'none'}
|
|
2296
|
+
+ `Steps completed so far: ${artifacts.map((a) => a.id).join(', ') || 'none'}.`
|
|
2297
|
+
+ gapBlock;
|
|
1829
2298
|
// The progress line IS the adaptation channel (addendum 2): the balance rides
|
|
1830
2299
|
// in via obtainPlan's live read, and this says where the run got to. Both are
|
|
1831
2300
|
// the planner re-allocating what remains across what is left — never a rate.
|
|
2301
|
+
//
|
|
2302
|
+
// F85 adds the second half. This line used to count STEPS only ("step 1 of 1
|
|
2303
|
+
// did not finish; 0 step(s) completed before it") — structurally true and
|
|
2304
|
+
// silent about outcome, so a replanner reading it could only conclude that
|
|
2305
|
+
// nothing had been achieved. On u-msh70zla it concluded exactly that and threw
|
|
2306
|
+
// three attempts of real convergence away. The structural sentence stays (a
|
|
2307
|
+
// plan's shape is what the planner re-allocates); the measured one joins it.
|
|
1832
2308
|
const progress = `step ${idx + 1} of ${plan.steps.length} ("${step.id}") did not finish; `
|
|
1833
|
-
+ `${artifacts.length} step(s) completed before it
|
|
2309
|
+
+ `${artifacts.length} step(s) completed before it; `
|
|
2310
|
+
+ `close trend so far: ${stopTrend.reading}`;
|
|
1834
2311
|
let pv;
|
|
1835
2312
|
try {
|
|
1836
2313
|
pv = await obtainPlan('replan', failure, progress);
|
|
@@ -1886,8 +2363,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1886
2363
|
emitMoneyHalt({ phase: `step:${step.id}`, cutMidCall: false, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
1887
2364
|
return 'cap-halt';
|
|
1888
2365
|
}
|
|
1889
|
-
// A
|
|
1890
|
-
//
|
|
2366
|
+
// A `step-variance` that earned no replan above is a STOP, and the stop is the
|
|
2367
|
+
// result. Reached in three ways now: the ordinary ceiling with the run reading
|
|
2368
|
+
// `flat` or `unknown`; the arbiter's one grant already spent (F85's bound); or
|
|
2369
|
+
// the wallet at dust. Each rides out as `step-red:<id>`, NOT as its own top-level
|
|
1891
2370
|
// outcome. The category is a replan TRIGGER, never a run verdict: leaking it
|
|
1892
2371
|
// upward would mint an outcome run.js and the ledger's class table do not know,
|
|
1893
2372
|
// and an unmapped category is counted as a library bug (ledger.js) when this is a
|