bareloop 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +625 -0
- package/README.md +101 -110
- package/bareloop.context.md +333 -25
- package/package.json +1 -1
- package/src/authorflow.js +1447 -0
- package/src/authoring.js +1486 -0
- package/src/authorjob.js +801 -0
- package/src/authorscout.js +767 -0
- package/src/bridges.js +16 -3
- package/src/declaredclose.js +519 -0
- package/src/index.js +75 -0
- package/src/job.js +48 -4
- package/src/kinds.js +1445 -0
- package/src/ledger.js +18 -1
- package/src/plan.js +26 -1
- package/src/planrun.js +448 -40
- package/src/ralph.js +24 -4
- package/src/reuse.js +120 -9
- package/src/root.js +8 -1
- package/src/run.js +12 -4
- package/src/text.js +199 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/src/workbranch.js +261 -0
- package/types/authorflow.d.ts +764 -0
- package/types/authoring.d.ts +559 -0
- package/types/authorjob.d.ts +242 -0
- package/types/authorscout.d.ts +320 -0
- package/types/declaredclose.d.ts +174 -0
- package/types/index.d.ts +9 -1
- package/types/job.d.ts +1 -0
- package/types/kinds.d.ts +395 -0
- package/types/plan.d.ts +18 -0
- package/types/planrun.d.ts +53 -3
- package/types/ralph.d.ts +1 -0
- package/types/reuse.d.ts +1 -1
- package/types/run.d.ts +10 -2
- package/types/text.d.ts +151 -0
- package/types/validate.d.ts +74 -9
- package/types/workbranch.d.ts +102 -0
package/src/planrun.js
CHANGED
|
@@ -20,7 +20,7 @@ import { join, resolve, relative } from 'node:path';
|
|
|
20
20
|
import { Gate } from 'bareguard';
|
|
21
21
|
import { LiteCtx } from 'litectx';
|
|
22
22
|
import { runClose, runStages, ralph, CLOSE_FAULTS, boundGap } from './ralph.js';
|
|
23
|
-
import { validatePlan, legalScopes,
|
|
23
|
+
import { validatePlan, legalScopes, closeStagesOf } from './plan.js';
|
|
24
24
|
import { WRITE_VERBS, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, MAX_SCOPE_MENU } from './plan.js';
|
|
25
25
|
import { snapshotScope, evalExits, CHECK_GAP_MAX } from './exits.js';
|
|
26
26
|
import { createRoot } from './root.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, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES } 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');
|
|
@@ -257,6 +260,60 @@ export function closeGapBlock(gap) {
|
|
|
257
260
|
+ (scrubbed.length > CHECK_GAP_MAX ? boundGap(scrubbed, REPLAN_GAP_KEEP) : scrubbed);
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
|
|
265
|
+
* provider string (`denied:<tool>` today, ~20 bytes), not a gap, so this is a
|
|
266
|
+
* backstop against a future terminal that arrives long — never an envelope the
|
|
267
|
+
* shipped path routinely rides through.
|
|
268
|
+
*/
|
|
269
|
+
export const BOUND_REASON_MAX = 200;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What the NEXT attempt is told about the bound that cut the previous one.
|
|
273
|
+
*
|
|
274
|
+
* Two different bounds used to set the same bare `attemptBounded = roundIteration`
|
|
275
|
+
* and therefore render the same sentence. For a round-bound cutoff that sentence is
|
|
276
|
+
* true. For BA-11's deny streak it is false twice over — the rounds did NOT run out
|
|
277
|
+
* (the fence short-circuited the attempt with budget left), and the count it quotes
|
|
278
|
+
* is the cap, not what was spent — and it aims the worker at the READ BUDGET when the
|
|
279
|
+
* thing that stopped it was the FENCE. The reason was already recorded on the spine;
|
|
280
|
+
* nothing carried it to the one reader who could act on it.
|
|
281
|
+
*
|
|
282
|
+
* The denial note is MECHANICAL in the strict sense: it quotes the terminal the
|
|
283
|
+
* provider actually returned and names which bound fired. It invents nothing —
|
|
284
|
+
* no streak count and no denied path, because bare-agent's return (`error:
|
|
285
|
+
* 'denied:<tool>'`, loop.js) carries neither, and a number nobody measured is a
|
|
286
|
+
* bug with a confident voice. Every other cause keeps the frozen wording byte for
|
|
287
|
+
* byte: a wall-bounded attempt reads as a round bound exactly as it did before
|
|
288
|
+
* (and is unreachable anyway — the step head refuses to fund an attempt past the
|
|
289
|
+
* deadline, W-2), so this change can only ever speak where it has something true
|
|
290
|
+
* to add.
|
|
291
|
+
*
|
|
292
|
+
* The reason arrives ALREADY SCRUBBED (the capture seam in `mkWorker` runs it
|
|
293
|
+
* through the one `SECRET_PATTERNS` inventory), so this renderer only bounds it —
|
|
294
|
+
* and announces the trim with the repo's ONE marker rather than a second spelling
|
|
295
|
+
* of "this text was cut" (F90.2).
|
|
296
|
+
*
|
|
297
|
+
* @param {{iteration?: number|string|undefined, cause: string, reason: string|null}|null|undefined} bounded
|
|
298
|
+
* the previous attempt's bound state, or nullish when it was not bounded
|
|
299
|
+
* @param {number} rounds the step's round bound, as quoted by the frozen sentence
|
|
300
|
+
* @returns {string|null} the note, or null when there is nothing to say
|
|
301
|
+
*/
|
|
302
|
+
export function boundedNote(bounded, rounds) {
|
|
303
|
+
if (!bounded) return null;
|
|
304
|
+
if (bounded.cause !== 'denied') {
|
|
305
|
+
return `Your previous attempt was CUT OFF after ${rounds} tool rounds. `
|
|
306
|
+
+ 'Reading is bounded; writing is not. Form a hypothesis EARLY and make the change.';
|
|
307
|
+
}
|
|
308
|
+
const raw = String(bounded.reason ?? '');
|
|
309
|
+
const shown = raw.length > BOUND_REASON_MAX
|
|
310
|
+
? `${raw.slice(0, BOUND_REASON_MAX)} [${GAP_TRIM_MARKER} ${raw.length - BOUND_REASON_MAX} of ${raw.length} characters withheld — the cap is ${BOUND_REASON_MAX}]`
|
|
311
|
+
: raw;
|
|
312
|
+
return 'Your previous attempt was CUT OFF by the gate: it denied consecutive tool calls '
|
|
313
|
+
+ `until the streak guard ended the attempt. The gate recorded: ${shown}. `
|
|
314
|
+
+ 'The round budget was not what stopped you — repeating a denied call ends this attempt the same way.';
|
|
315
|
+
}
|
|
316
|
+
|
|
260
317
|
/**
|
|
261
318
|
* The plan-drafting prompt: a schema DESCRIPTION built from the live validator
|
|
262
319
|
* menus — never a copyable example (the drafter must author, not echo; the
|
|
@@ -306,7 +363,7 @@ export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes,
|
|
|
306
363
|
// executes and `validatePlan` accepts against. Reading `job.close` raw here left
|
|
307
364
|
// the legacy object form offering nothing while the runner preflighted a stage.
|
|
308
365
|
// (`?? []`: a close naming no command stages nothing, so it offers nothing)
|
|
309
|
-
const closeStages =
|
|
366
|
+
const closeStages = closeStagesOf(job) ?? [];
|
|
310
367
|
const checkNames = checkMenu(closeStages).map((m) => m.name);
|
|
311
368
|
const doc = `DRAFT-PLAN
|
|
312
369
|
You are planning how to accomplish a goal in a repository, as an ordered list of bounded
|
|
@@ -318,7 +375,7 @@ Shape: { "schema": "plan-v1", "steps": [ ... 1..${MAX_PLAN_STEPS} steps ... ] }
|
|
|
318
375
|
strictly in array order. Each step (no other fields exist):
|
|
319
376
|
- "id": kebab-case slug, unique
|
|
320
377
|
- "action": the step's task, precise enough for a worker that sees ONLY this step
|
|
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)
|
|
378
|
+
- "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (read/grep are the worker's ONLY way to see the tree — there is no shell; 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)
|
|
322
379
|
- "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
|
|
323
380
|
- "target": the step's deliverable path (REQUIRED when tools include write/edit), inside ${JSON.stringify(job.writeScope)}
|
|
324
381
|
- "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
|
|
@@ -348,6 +405,15 @@ strictly in array order. Each step (no other fields exist):
|
|
|
348
405
|
exit reds on that file can never finish, on any attempt.${seedRedLaw}${priorChecksLaw}
|
|
349
406
|
Reference checks by NAME only; you cannot author or modify one.
|
|
350
407
|
|
|
408
|
+
The worker has NO SHELL. It cannot run a command, a compiler, a linter, or a test suite,
|
|
409
|
+
and it cannot execute a script it writes: its only verbs are the tools you grant that step
|
|
410
|
+
in "tools". The check named in a "check-passes" exit is the ONLY execution in this run —
|
|
411
|
+
the shell runs it after each attempt and hands the worker its raw output. So write every
|
|
412
|
+
"action" for a worker that locates its work by READING and GREPPING files and by reading
|
|
413
|
+
the check output it is given. Never tell it to run, execute, re-run, or verify by running
|
|
414
|
+
anything (an action that opens with "Run \`<command>\`…" costs the whole step: the worker
|
|
415
|
+
has no way to obey it and spends its rounds hunting by hand).
|
|
416
|
+
|
|
351
417
|
Offered "scope" values for tree-changed — copy ONE of these exactly, character for
|
|
352
418
|
character. No other value is accepted, and patterns of your own (like "src/*.js")
|
|
353
419
|
are not accepted:
|
|
@@ -509,12 +575,18 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
509
575
|
* default; a caller supplies this to drive time deterministically (the same seam `createClock`
|
|
510
576
|
* already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
|
|
511
577
|
* whose floor is one close timeout).
|
|
578
|
+
* @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed leg
|
|
579
|
+
* recorded on its own spine (`readResume`'s `restart.branch`, off the `work-branch`
|
|
580
|
+
* event). With it the resume returns to the branch its own work is sitting on; without
|
|
581
|
+
* it the deterministic name would collide with that very branch and the collision walk
|
|
582
|
+
* would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
|
|
583
|
+
* path. A recorded branch that no longer exists is a STOP, never a fresh start.
|
|
512
584
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
513
585
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
514
|
-
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
515
|
-
* `step-red:<id>`
|
|
586
|
+
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
587
|
+
* 'step-stalled' | `step-red:<id>`
|
|
516
588
|
*/
|
|
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 }) {
|
|
589
|
+
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 }) {
|
|
518
590
|
workdir = resolve(workdir);
|
|
519
591
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
520
592
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
@@ -550,14 +622,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
550
622
|
// A STAGED close (PRD v1.28) is an ordered list of command stages and is the
|
|
551
623
|
// go-forward shape; the object form survives only for the declared-but-locked
|
|
552
624
|
// verdict classes (gold/rubric/hitl), which name no command to run.
|
|
553
|
-
// W4: the staging itself lives in ONE place (`
|
|
625
|
+
// W4: the staging itself lives in ONE place (`closeStagesOf`), shared with
|
|
554
626
|
// `planPrompt` and `validatePlan` — a second copy here is exactly how the
|
|
555
|
-
// runner came to execute a stage neither of the other two could see.
|
|
556
|
-
|
|
627
|
+
// runner came to execute a stage neither of the other two could see. M4 widened
|
|
628
|
+
// that one derivation to the AUTHORED close (`closeDecl`) rather than adding a
|
|
629
|
+
// second: a consumer reading only `close` sees a declared job as closeless.
|
|
630
|
+
const stagedClose = closeStagesOf(job);
|
|
631
|
+
const declared = isDeclaredClose(job);
|
|
557
632
|
if (!stagedClose) {
|
|
558
633
|
emit('escalation', {
|
|
559
634
|
category: 'close-unsupported', decisionReady: true,
|
|
560
|
-
decision: `The job's close is a ${job.close
|
|
635
|
+
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).`,
|
|
561
636
|
options: ['restate the close as a predicate', 'wait for the verdict-classes rung'],
|
|
562
637
|
});
|
|
563
638
|
return 'close-unsupported';
|
|
@@ -738,6 +813,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
738
813
|
];
|
|
739
814
|
|
|
740
815
|
const closeOpts = { timeoutMs: closeTimeoutMs, cwd: workdir };
|
|
816
|
+
/** D8 — the DECLARED close's seed, filled in below before anything runs.
|
|
817
|
+
* `null` for a command close, which has no baseline to measure against. */
|
|
818
|
+
/** @type {{seedRef: string}|null} */
|
|
819
|
+
let declaredCtx = null;
|
|
820
|
+
/**
|
|
821
|
+
* ONE close-execution seam, chosen once by which field the SIGNED spec carries.
|
|
822
|
+
* Both executors return `runClose`'s verdict shape, so everything downstream —
|
|
823
|
+
* the precheck, the preflight, the check seam, ralph's judge, `CLOSE_FAULTS`,
|
|
824
|
+
* the trend reader and Layer R — is the same code for both.
|
|
825
|
+
*
|
|
826
|
+
* The command path is byte-identical to what it was: same call, same options.
|
|
827
|
+
* @param {any[]} stages
|
|
828
|
+
*/
|
|
829
|
+
const runCloseStages = (stages) => (declared
|
|
830
|
+
? runDeclaredStages(stages, scrub, { ...closeOpts, seedRef: /** @type {any} */ (declaredCtx).seedRef })
|
|
831
|
+
: runStages(stages, scrub, closeOpts));
|
|
741
832
|
/** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
|
|
742
833
|
let closeStage;
|
|
743
834
|
/** @type {any} the LAST close verdict itself (W-2). A wall stop KEEPS the grade
|
|
@@ -795,16 +886,172 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
795
886
|
};
|
|
796
887
|
/** the close, as ONE verdict: every stage in order, first red wins */
|
|
797
888
|
const judgeClose = async () => {
|
|
798
|
-
const v = await
|
|
889
|
+
const v = await runCloseStages(stagedClose);
|
|
799
890
|
closeStage = v.stage;
|
|
800
891
|
lastCloseVerdict = v;
|
|
801
892
|
// Only a RED grade is a reading. A `satisfied` ends the run and a close FAULT
|
|
802
893
|
// rendered no judgment at all (CLOSE_FAULTS) — folding either into the series
|
|
803
894
|
// would put a non-number where the instrument expects a graded one.
|
|
804
|
-
|
|
895
|
+
// `closeGrade` is the reader's input for EITHER executor: a command close
|
|
896
|
+
// hands over its gap and `readGrade` scrapes the stage and the count out of
|
|
897
|
+
// it; a declared close already KNOWS both and says them, rather than
|
|
898
|
+
// round-tripping a number through prose it would then have to parse back.
|
|
899
|
+
if (v.verdict === 'needs_revision') runTrend.record(closeGrade(v));
|
|
805
900
|
return v;
|
|
806
901
|
};
|
|
807
902
|
|
|
903
|
+
// ── 0c. THE WORK BRANCH — the HARD RULE (PRD Addendum v1.57 §3, hamr):
|
|
904
|
+
// *"The agent creates a NEW BRANCH before it touches any code — a HARD RULE, no
|
|
905
|
+
// exceptions. Not a default, not a preference: no job edits the branch it was
|
|
906
|
+
// handed, and none edits `main`."*
|
|
907
|
+
//
|
|
908
|
+
// ONE SEAM, TWO PLACEMENTS. The split is COLD vs RESUME, and every half of it is
|
|
909
|
+
// ruled rather than convenient:
|
|
910
|
+
//
|
|
911
|
+
// * BEFORE the scout on BOTH paths, the scout being the run's first PAID call. A
|
|
912
|
+
// patient that is not a git checkout, a namespace with no free name, a resume
|
|
913
|
+
// whose branch a human deleted — each is an instrument stop that must cost zero
|
|
914
|
+
// tokens, and a branch prepared after the scout would already have bought one.
|
|
915
|
+
// * COLD: AFTER the precheck and the preflight, which are $0 and deterministic.
|
|
916
|
+
// An ALREADY-GREEN tree returns below without ever reaching the seam and leaves
|
|
917
|
+
// no branch behind: a run with no work to do has no blast radius to bound, and
|
|
918
|
+
// minting a branch for it would litter every patient with the record of a run
|
|
919
|
+
// that did nothing.
|
|
920
|
+
// * RESUME: BEFORE them — here, above 0*. There is nothing to mint on a resume
|
|
921
|
+
// (the recorded branch exists, `created:false`, and the arm below refuses
|
|
922
|
+
// rather than creating), so the leave-no-branch-behind clause has nothing to
|
|
923
|
+
// say; what IS at stake is WHICH TREE the $0 instruments measure. The recorded
|
|
924
|
+
// branch holds the work the operator already paid for and IS the run being
|
|
925
|
+
// continued, while the ref the operator handed back can be anything at all. Run
|
|
926
|
+
// from where the run happens to stand, the precheck could read `already-green`
|
|
927
|
+
// off a tree that is not this run's and every `baseline: "seed"` stage would
|
|
928
|
+
// baseline against it — an instrument measuring the wrong subject, reading
|
|
929
|
+
// honestly and saying nothing true. Moving the seam up is also strictly cheaper
|
|
930
|
+
// for the failure case: a resume whose branch is gone or foreign now stops
|
|
931
|
+
// before the arbiter grades anything at all.
|
|
932
|
+
//
|
|
933
|
+
// The scout itself needs no branch and is not what this gates: its menu simply
|
|
934
|
+
// has no write-class verb in it (the menu IS the grant), so it cannot touch code
|
|
935
|
+
// wherever it stands. What the branch gates is the WRITE-CAPABLE phase, and that
|
|
936
|
+
// gate is enforced structurally rather than by this ordering — `mkWorker` refuses
|
|
937
|
+
// to build a `writable` worker while `workBranch` is null (below). Ordering says
|
|
938
|
+
// where the honest stop happens; the guard says the rule cannot be bypassed.
|
|
939
|
+
//
|
|
940
|
+
// Creating a branch at HEAD moves no commit and changes nothing the close can
|
|
941
|
+
// measure: the declared seed is HEAD either way, and the tree — dirty or clean —
|
|
942
|
+
// is carried across untouched.
|
|
943
|
+
/** @type {string|null} the branch this run's work lands on, once it exists */
|
|
944
|
+
let workBranch = null;
|
|
945
|
+
/** is this leg continuing work that already exists? Spelled EXACTLY as
|
|
946
|
+
* `prepareWorkBranch`'s own resume test, so the two cannot disagree about which
|
|
947
|
+
* arm a given input takes. */
|
|
948
|
+
const resuming = resumeBranch !== null && resumeBranch !== undefined;
|
|
949
|
+
/**
|
|
950
|
+
* 0c itself — ONE definition for both placements. The refusal below is written
|
|
951
|
+
* once on purpose: two call sites emitting their own branch-red would be two
|
|
952
|
+
* messages free to drift apart, and this one is what a human reads when a run
|
|
953
|
+
* stops having spent nothing.
|
|
954
|
+
* @returns {Promise<'branch-red'|null>} the terminal to return, or `null` once the
|
|
955
|
+
* run is standing on a branch nobody handed it
|
|
956
|
+
*/
|
|
957
|
+
const prepareBranch = async () => {
|
|
958
|
+
const wb = await prepareWorkBranch(workdir, { name: workBranchName(job), resume: resumeBranch ?? null });
|
|
959
|
+
if (wb.stop !== null) {
|
|
960
|
+
// Never a fallback to the handed branch. That fallback IS the thing the rule
|
|
961
|
+
// forbids, and a run that silently took it would report a green whose blast
|
|
962
|
+
// radius nobody bounded — so this is a named terminal of its own rather than
|
|
963
|
+
// a wiring fault (`interpreter-red` aims an upstream ask at a library; a
|
|
964
|
+
// patient that is not a git checkout is the operator's own state speaking).
|
|
965
|
+
emit('escalation', {
|
|
966
|
+
category: 'branch-red', decisionReady: true,
|
|
967
|
+
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.',
|
|
968
|
+
options: [
|
|
969
|
+
'make the patient a git checkout with at least one commit',
|
|
970
|
+
'free the work-branch name (delete or rename the stale branches this run collided with)',
|
|
971
|
+
'abandon the run',
|
|
972
|
+
],
|
|
973
|
+
detail: scrub(wb.stop),
|
|
974
|
+
});
|
|
975
|
+
return 'branch-red';
|
|
976
|
+
}
|
|
977
|
+
workBranch = wb.branch;
|
|
978
|
+
// The run's own books say where its work went. A human reading a spine — or a
|
|
979
|
+
// resume reading it back (`readResume`'s `restart.branch`) — must not have to
|
|
980
|
+
// re-derive the name from the spec and guess how many collisions there were.
|
|
981
|
+
emit('work-branch', {
|
|
982
|
+
branch: wb.branch,
|
|
983
|
+
created: wb.created,
|
|
984
|
+
resumed: wb.resumed,
|
|
985
|
+
from: wb.from,
|
|
986
|
+
base: wb.base,
|
|
987
|
+
repo: wb.repo,
|
|
988
|
+
...(wb.collided > 0 ? { collided: wb.collided } : {}),
|
|
989
|
+
meaning: wb.resumed
|
|
990
|
+
? 'returned to the branch this run was killed on — its work is on this branch, and a fresh one would strand it'
|
|
991
|
+
: 'the HARD RULE (PRD v1.57 §3): the run works here, never on the branch it was handed and never on main',
|
|
992
|
+
});
|
|
993
|
+
return null;
|
|
994
|
+
};
|
|
995
|
+
if (resuming) {
|
|
996
|
+
const stop = await prepareBranch();
|
|
997
|
+
if (stop !== null) return stop;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── 0*. the DECLARED close's two run-start facts. Both are $0 and both sit
|
|
1001
|
+
// INSIDE the clock (they are the arbiter's own instruments, and time the
|
|
1002
|
+
// operator's instruments too or the budget measures a different run).
|
|
1003
|
+
//
|
|
1004
|
+
// (1) THE SEED (D8/D12). HEAD at run start, READ and recorded, never typed.
|
|
1005
|
+
// The close stores the counting RULE and never the number, so every
|
|
1006
|
+
// `baseline: "seed"` stage is measured against THIS run's own starting
|
|
1007
|
+
// point — a constant frozen at signing would judge run 5 against run 1's
|
|
1008
|
+
// tree, which is the one-experiment shape D12 retires.
|
|
1009
|
+
//
|
|
1010
|
+
// (2) THE GROUNDED RE-VALIDATION (D9 gate 1). The job validator judged the
|
|
1011
|
+
// declaration with no repository in hand, so hamr's listing rule and the
|
|
1012
|
+
// scoped-job derivation that arms the F84 one-population law were
|
|
1013
|
+
// DEFERRED, not skipped. They run here, against the real seed tree,
|
|
1014
|
+
// before any stage and before any token. A declaration that names a path
|
|
1015
|
+
// the tree does not have is a broken close, not a red about the worker —
|
|
1016
|
+
// round 2's arm A invented `src/alertEmail.js` and silently reclassified
|
|
1017
|
+
// 15 real errors into the wrong population.
|
|
1018
|
+
if (declared) {
|
|
1019
|
+
const s = await seedAtHead(workdir);
|
|
1020
|
+
if (s.stop !== null) {
|
|
1021
|
+
emit('escalation', {
|
|
1022
|
+
category: CLOSE_FAULTS.failed.category, decisionReady: true,
|
|
1023
|
+
decision: `The authored close measures against this run's own seed, and the seed could not be read. ${CLOSE_FAULTS.failed.decision}`,
|
|
1024
|
+
options: ['check the repository is a git checkout with at least one commit', ...CLOSE_FAULTS.failed.options],
|
|
1025
|
+
detail: scrub(s.stop),
|
|
1026
|
+
});
|
|
1027
|
+
return 'close-red';
|
|
1028
|
+
}
|
|
1029
|
+
const listed = await seedListing(workdir, s.seedRef);
|
|
1030
|
+
const v = listed.stop === null
|
|
1031
|
+
? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType })
|
|
1032
|
+
: { ok: false, grounded: false, reds: [{ code: 'listing-unreadable', path: 'closeDecl', detail: listed.stop }] };
|
|
1033
|
+
emit('close-decl', {
|
|
1034
|
+
genre: job.closeDecl.genre,
|
|
1035
|
+
lang: job.closeDecl.lang,
|
|
1036
|
+
seedRef: s.seedRef,
|
|
1037
|
+
stages: stagedClose.map((/** @type {any} */ st) => st.name),
|
|
1038
|
+
grounded: v.grounded,
|
|
1039
|
+
ok: v.ok,
|
|
1040
|
+
...(v.ok ? {} : { reds: v.reds.map((/** @type {any} */ r) => ({ ...r, detail: scrub(String(r.detail ?? '')) })) }),
|
|
1041
|
+
});
|
|
1042
|
+
if (!v.ok) {
|
|
1043
|
+
emit('escalation', {
|
|
1044
|
+
category: CLOSE_FAULTS.failed.category, decisionReady: true,
|
|
1045
|
+
decision: 'The authored close does not validate against the repository it is about to judge — a path it names, '
|
|
1046
|
+
+ 'or a population it counts, does not match the seed tree. Nothing it rendered would be trustworthy.',
|
|
1047
|
+
options: ['re-author the close against this repository (a new declaration is a new spec hash, and needs re-signing)', ...CLOSE_FAULTS.failed.options],
|
|
1048
|
+
detail: v.reds.map((/** @type {any} */ r) => `${r.code}:${r.path}${r.detail ? ` — ${scrub(String(r.detail))}` : ''}`).join('\n'),
|
|
1049
|
+
});
|
|
1050
|
+
return 'close-red';
|
|
1051
|
+
}
|
|
1052
|
+
declaredCtx = { seedRef: s.seedRef };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
808
1055
|
// ── 0a. close precheck (close-first, F17): already-green is a DISTINCT
|
|
809
1056
|
// record, zero tokens; a forbidden-zone verdict escalates before any spend
|
|
810
1057
|
const pre = await judgeClose();
|
|
@@ -838,7 +1085,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
838
1085
|
: {}),
|
|
839
1086
|
});
|
|
840
1087
|
for (const m of menu) {
|
|
841
|
-
const v = await
|
|
1088
|
+
const v = await runCloseStages(m.run);
|
|
842
1089
|
if (v.verdict === 'needs_revision') {
|
|
843
1090
|
seedRed.push(m.name);
|
|
844
1091
|
// …and the preflight grade is a READING, for the same reason the precheck one
|
|
@@ -856,7 +1103,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
856
1103
|
// handed to a human and to the replanner, which is the exact class of false
|
|
857
1104
|
// story this change exists to remove. Asked of the reader's own books, so
|
|
858
1105
|
// there is no second record of what has been graded.
|
|
859
|
-
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record(
|
|
1106
|
+
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record(closeGrade(v));
|
|
860
1107
|
}
|
|
861
1108
|
emit('check-preflight', { name: m.name, verdict: v.verdict, ...(m.run.length > 1 ? { chain: m.run.map((s2) => s2.name) } : {}) });
|
|
862
1109
|
const f = Object.hasOwn(CLOSE_FAULTS, v.verdict) ? CLOSE_FAULTS[v.verdict] : undefined;
|
|
@@ -866,10 +1113,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
866
1113
|
}
|
|
867
1114
|
}
|
|
868
1115
|
|
|
1116
|
+
// ── 0c, COLD. The seam and both halves of its placement ruling are stated
|
|
1117
|
+
// above, where a RESUME reaches it; this is where a run that is NOT resuming
|
|
1118
|
+
// does — after the $0 precheck and preflight, so an already-green tree has
|
|
1119
|
+
// already returned and left no branch behind.
|
|
1120
|
+
if (!resuming) {
|
|
1121
|
+
const stop = await prepareBranch();
|
|
1122
|
+
if (stop !== null) return stop;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
869
1125
|
const lc = new LiteCtx({ root: workdir });
|
|
870
1126
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
871
1127
|
const fencePrefixes = job.writeScope.map((/** @type {string} */ g) => resolve(workdir, globToPrefix(g)));
|
|
872
|
-
const auditPath = join(workdir,
|
|
1128
|
+
const auditPath = join(workdir, GATE_AUDIT_FILE);
|
|
873
1129
|
const chainByName = new Map(menu.map((m) => [m.name, m.run]));
|
|
874
1130
|
// Choose-don't-describe (§4): the offered `tree-changed` scopes, enumerated from
|
|
875
1131
|
// the signed fence plus the directories that actually exist beneath it. ONE menu
|
|
@@ -909,7 +1165,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
909
1165
|
// derivation, not a live path; the reachable guard is the validator's, and
|
|
910
1166
|
// that one is mutation-covered.
|
|
911
1167
|
if (!chain) return { pass: false, fault: 'failed', gap: `no offered close stage named "${name}"` };
|
|
912
|
-
const v = await
|
|
1168
|
+
const v = await runCloseStages(chain);
|
|
913
1169
|
emit('check-run', { name, verdict: v.verdict, ...(v.stage && v.stage !== name ? { stage: v.stage } : {}), ...(v.exitCode !== undefined ? { exitCode: v.exitCode } : {}) });
|
|
914
1170
|
if (v.verdict === 'satisfied') return { pass: true };
|
|
915
1171
|
if (v.verdict === 'needs_revision') {
|
|
@@ -927,7 +1183,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
927
1183
|
// step grade donates a null. Measured on u-msh70zla's own archived gaps: raw
|
|
928
1184
|
// reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Same seam the
|
|
929
1185
|
// Layer R note below already calls out for a check's `^`-anchored gapKeep.
|
|
930
|
-
runTrend.record(
|
|
1186
|
+
runTrend.record(closeGrade(v));
|
|
931
1187
|
return { pass: false, gap: v.gap };
|
|
932
1188
|
}
|
|
933
1189
|
return { pass: false, fault: v.verdict, gap: v.detail };
|
|
@@ -955,6 +1211,27 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
955
1211
|
* the turn-allowance note below.
|
|
956
1212
|
*/
|
|
957
1213
|
async function mkWorker({ granted, phase, attemptRounds, attempts, writable, root = null, fence = null, workerProvider = null }) {
|
|
1214
|
+
// THE HARD RULE, ENFORCED STRUCTURALLY (PRD v1.57 §3). This is the ONE seam
|
|
1215
|
+
// that grants write-class verbs — `writeScope` is empty for every other
|
|
1216
|
+
// worker, so a read-only phase cannot reach a file whatever it intends — and
|
|
1217
|
+
// a branch is therefore a PRECONDITION of it, not a courtesy the ordering
|
|
1218
|
+
// above happens to provide. "No exceptions" has to mean the rule survives a
|
|
1219
|
+
// future caller who adds a third writable worker and forgets the ordering.
|
|
1220
|
+
//
|
|
1221
|
+
// Unreachable through `runPlan` as it stands (0c runs before the scout and
|
|
1222
|
+
// returns `branch-red` on any fault), which is exactly what a backstop should
|
|
1223
|
+
// be. Categorised so it lands as the wiring fault it is: the step site and
|
|
1224
|
+
// `relay` both return `interpreter-red` verbatim, so the escalation a human
|
|
1225
|
+
// reads and the outcome the spine records name the same thing.
|
|
1226
|
+
if (writable && workBranch === null) {
|
|
1227
|
+
const err = /** @type {any} */ (new Error(
|
|
1228
|
+
`HARD RULE (PRD v1.57 §3): a write-capable worker was requested for phase "${phase}" with no work branch prepared — `
|
|
1229
|
+
+ 'no job edits the branch it was handed. The run is refused rather than allowed to write where it stands.',
|
|
1230
|
+
));
|
|
1231
|
+
err.category = 'interpreter-red';
|
|
1232
|
+
err.lib = 'bareloop';
|
|
1233
|
+
throw err;
|
|
1234
|
+
}
|
|
958
1235
|
/**
|
|
959
1236
|
* The gate's LLM-turn allowance THROUGH iteration `i` — the old expression
|
|
960
1237
|
* `attemptRounds * (attempts + 1)` with the iteration count made explicit
|
|
@@ -984,7 +1261,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
984
1261
|
// uses), so this can only tighten, never widen
|
|
985
1262
|
writeScope: writable ? (fence ?? fencePrefixes) : [],
|
|
986
1263
|
readScope: [workdir],
|
|
987
|
-
deny: [auditPath,
|
|
1264
|
+
deny: [auditPath, ...ARBITER_BOOK_STORES.map((s) => join(workdir, s))],
|
|
988
1265
|
},
|
|
989
1266
|
budget: { maxCostUsd: Math.max(remainingUsd(), 0.0001) },
|
|
990
1267
|
limits: { maxTurns },
|
|
@@ -1098,7 +1375,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1098
1375
|
/** @type {number|string|undefined} */
|
|
1099
1376
|
let roundIteration;
|
|
1100
1377
|
let roundsThisAttempt = 0;
|
|
1101
|
-
/**
|
|
1378
|
+
/**
|
|
1379
|
+
* WHICH bound cut the last attempt, and — when the provider recorded one — its
|
|
1380
|
+
* reason. A bare iteration number was the whole state here, so three different
|
|
1381
|
+
* bounds (round cap, wall, BA-11 deny streak) were indistinguishable to the one
|
|
1382
|
+
* consumer that speaks to the worker, which then told every one of them the
|
|
1383
|
+
* round cap had run out. The cause travels WITH the iteration because they are
|
|
1384
|
+
* read together and can never be allowed to drift apart.
|
|
1385
|
+
*
|
|
1386
|
+
* `reason` is scrubbed at THIS seam — the boundary, once, before any reader —
|
|
1387
|
+
* so no consumer has to remember to do it (the same placement the exit results
|
|
1388
|
+
* get in `judge`). It rides the spine event in the same scrubbed spelling: one
|
|
1389
|
+
* value, one inventory, and an append-only log is forever.
|
|
1390
|
+
* `iteration` mirrors `roundIteration` exactly, undefined included: the bound
|
|
1391
|
+
* cannot know a label the run never set, and inventing one here would be the
|
|
1392
|
+
* only place in this file that claims to.
|
|
1393
|
+
* @type {{iteration: number|string|undefined, cause: 'rounds'|'wall'|'denied', reason: string|null}|undefined}
|
|
1394
|
+
*/
|
|
1102
1395
|
let attemptBounded;
|
|
1103
1396
|
/**
|
|
1104
1397
|
* Open an iteration: label the rounds, reset the per-iteration round counter,
|
|
@@ -1199,19 +1492,24 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1199
1492
|
}
|
|
1200
1493
|
// a maxTurns session is a BOUNDED attempt, not an escalation — the same
|
|
1201
1494
|
// role loop.stop() plays on the Loop path: judge the partial work and
|
|
1202
|
-
// feed the gap forward (the CLI preserves lastText, BA-5).
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
emit('attempt-bounded', { phase, iteration: roundIteration, cap: attemptRounds, native: true });
|
|
1206
|
-
return r;
|
|
1207
|
-
}
|
|
1495
|
+
// feed the gap forward (the CLI preserves lastText, BA-5). A DENIAL
|
|
1496
|
+
// STREAK is the same shape and takes the same lane (see the Loop path's
|
|
1497
|
+
// note below — one doctrine, both surfaces).
|
|
1208
1498
|
if (r.error) {
|
|
1499
|
+
if (r.error === 'max_turns' || r.error.startsWith('denied:')) {
|
|
1500
|
+
// the CAUSE is kept, not just the fact: `max_turns` really is the round
|
|
1501
|
+
// cap, a `denied:` terminal is the fence, and the next attempt is told
|
|
1502
|
+
// which one it was (`boundedNote`)
|
|
1503
|
+
const denied = r.error.startsWith('denied:');
|
|
1504
|
+
const reason = scrub(r.error);
|
|
1505
|
+
attemptBounded = { iteration: roundIteration, cause: denied ? 'denied' : 'rounds', reason };
|
|
1506
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, cap: attemptRounds, native: true, reason });
|
|
1507
|
+
return r;
|
|
1508
|
+
}
|
|
1209
1509
|
const err = /** @type {CategorizedError} */ (new Error(`native session: ${r.error}`));
|
|
1210
|
-
// halt → cap-halt
|
|
1211
|
-
//
|
|
1212
|
-
err.category = r.error.startsWith('halt:') ? 'cap-halt'
|
|
1213
|
-
: r.error.startsWith('denied:') ? 'gate-red'
|
|
1214
|
-
: 'provider-red';
|
|
1510
|
+
// halt → cap-halt; a bridge/session terminal (bridge-failed,
|
|
1511
|
+
// session_timeout, session:*) is provider-owned transport
|
|
1512
|
+
err.category = r.error.startsWith('halt:') ? 'cap-halt' : 'provider-red';
|
|
1215
1513
|
err.lib = 'bare-agent';
|
|
1216
1514
|
throw err;
|
|
1217
1515
|
}
|
|
@@ -1309,11 +1607,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1309
1607
|
// (a step that would BEGIN past the deadline is never funded), and the head of
|
|
1310
1608
|
// a close-fix iteration (W-2 — the run stops on the verdict already minted).
|
|
1311
1609
|
if (clock.expired()) {
|
|
1312
|
-
attemptBounded = roundIteration;
|
|
1610
|
+
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
1313
1611
|
emit('wall-bounded', { phase, iteration: roundIteration, ...clock.report(closeTimeoutForReport) });
|
|
1314
1612
|
self.stop();
|
|
1315
1613
|
} else if (roundsThisAttempt >= attemptRounds) {
|
|
1316
|
-
attemptBounded = roundIteration;
|
|
1614
|
+
attemptBounded = { iteration: roundIteration, cause: 'rounds', reason: null };
|
|
1317
1615
|
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds });
|
|
1318
1616
|
self.stop();
|
|
1319
1617
|
}
|
|
@@ -1347,12 +1645,25 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1347
1645
|
} catch (e) {
|
|
1348
1646
|
throw categorize(e).err;
|
|
1349
1647
|
}
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1648
|
+
// BA-11's deny streak is a BOUNDED ATTEMPT, not a terminal. It is the same
|
|
1649
|
+
// shape `loop.stop()` has at the round bound thirty lines up: the attempt
|
|
1650
|
+
// ran, produced real work, and was cut short by a governance bound doing
|
|
1651
|
+
// exactly its job — the fence HELD, which is the opposite of a fault. So it
|
|
1652
|
+
// takes the same lane: judge the partial work, feed the gap forward, the
|
|
1653
|
+
// loop continues under unchanged caps. Routing it as a terminal killed a
|
|
1654
|
+
// converging run with money and wall left (u-msn227nq). The native surface
|
|
1655
|
+
// takes the identical lane above — one doctrine, both surfaces.
|
|
1656
|
+
if (r.error && r.error.startsWith('denied:')) {
|
|
1657
|
+
const reason = scrub(r.error);
|
|
1658
|
+
attemptBounded = { iteration: roundIteration, cause: 'denied', reason };
|
|
1659
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds, reason });
|
|
1660
|
+
return r;
|
|
1661
|
+
}
|
|
1662
|
+
// the remaining error-return taxonomy (one map, same doctrine as native's):
|
|
1663
|
+
// halt → cap-halt, API truncation → provider-red, anything else is wiring
|
|
1352
1664
|
if (r.error) {
|
|
1353
1665
|
const err = /** @type {CategorizedError} */ (new Error(`worker loop: ${r.error}`));
|
|
1354
1666
|
err.category = r.error.startsWith('halt:') ? 'cap-halt'
|
|
1355
|
-
: r.error.startsWith('denied:') ? 'gate-red'
|
|
1356
1667
|
: r.error.startsWith('truncated:') ? 'provider-red'
|
|
1357
1668
|
: 'interpreter-red';
|
|
1358
1669
|
err.lib = 'bare-agent';
|
|
@@ -1388,6 +1699,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1388
1699
|
// and those are different diagnoses with different remedies (F45/F48: a
|
|
1389
1700
|
// casualty is never evidence, and a governance stop is never a casualty).
|
|
1390
1701
|
'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']],
|
|
1702
|
+
// A WIRING/INVARIANT fault is not a transport casualty, and the default
|
|
1703
|
+
// below would file it as one. The step loop already restores this category
|
|
1704
|
+
// verbatim at its own setup site ("the RECORDED outcome must match the
|
|
1705
|
+
// escalation the human reads", review #6/F11); the fix loop reached `relay`
|
|
1706
|
+
// instead and every such fault came back `provider-red` — a broken runner
|
|
1707
|
+
// reported as the provider's fault, and an upstream ask aimed at a library
|
|
1708
|
+
// for our own missing wiring (the step-stalled lesson).
|
|
1709
|
+
'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']],
|
|
1391
1710
|
};
|
|
1392
1711
|
const [decision, options] = (Object.hasOwn(DECIDE, category) ? DECIDE[category] : undefined)
|
|
1393
1712
|
?? [`The ${phase} call failed (${category}) — no result exists.`, ['retry the run', 'fix the provider binding', 'abandon the run']];
|
|
@@ -1398,7 +1717,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1398
1717
|
// `step-stalled` rides out as ITSELF, not as provider-red. Naming the
|
|
1399
1718
|
// escalation while returning a casualty label would launder a governance stop
|
|
1400
1719
|
// into transport noise at exactly the layer the readout reads.
|
|
1401
|
-
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled'
|
|
1720
|
+
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red'
|
|
1721
|
+
? category : 'provider-red';
|
|
1402
1722
|
};
|
|
1403
1723
|
|
|
1404
1724
|
// ── 1. SCOUT — read-only by construction: the write-class verbs are simply
|
|
@@ -1821,14 +2141,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1821
2141
|
// carries counts and paths only — never content (the spine is forever).
|
|
1822
2142
|
const rootInj = root ? root.observe({ iteration, gap, writes: w.workerWrites() }) : null;
|
|
1823
2143
|
if (rootInj) emit('root-injected', { step: step.id, ...rootInj.event });
|
|
2144
|
+
// read ONCE: the note and the "was it the attempt just before this one" test
|
|
2145
|
+
// are two questions about the same record, and two reads is how they drift
|
|
2146
|
+
const bound = w.wasBounded();
|
|
1824
2147
|
const r = await w.ask([
|
|
1825
2148
|
step.action,
|
|
1826
2149
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root — a relative path resolves against a different directory and will be denied by the gate.`,
|
|
1827
2150
|
step.target && `Write your deliverable to: ${resolve(workdir, step.target)}`,
|
|
1828
2151
|
artifacts.length > 0 && `Working context (read-only) — prior steps' results:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
|
|
1829
2152
|
gap && `Previous attempt failed this step's checks:\n${gap}`,
|
|
1830
|
-
|
|
1831
|
-
|
|
2153
|
+
// the note names the bound that ACTUALLY fired — a deny streak and a spent
|
|
2154
|
+
// round cap are different diagnoses with different remedies, and telling a
|
|
2155
|
+
// fenced worker to "form a hypothesis early" aims it at the wrong wall
|
|
2156
|
+
bound?.iteration === iteration - 1 && boundedNote(bound, step.rounds),
|
|
1832
2157
|
rootInj && rootInj.note,
|
|
1833
2158
|
].filter(Boolean).join('\n\n'));
|
|
1834
2159
|
lastText = scrub(r.text ?? '').slice(0, ARTIFACT_MAX);
|
|
@@ -2011,7 +2336,32 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2011
2336
|
&& cat === 'step-variance'
|
|
2012
2337
|
&& !varianceGrantUsed
|
|
2013
2338
|
&& stopTrend.trend === 'converging';
|
|
2014
|
-
|
|
2339
|
+
// W-2 at the replan GATE, hamr's ruling: *"past the wall no new fix cycle or step
|
|
2340
|
+
// starts"* — and a replan is the first move of starting a new step. Past the
|
|
2341
|
+
// deadline the whole cycle is doomed by construction and it is not free: the
|
|
2342
|
+
// drafting call is bought (money the operator's time allowance no longer covers),
|
|
2343
|
+
// the redrafted plan's first step is then refused at its own head by the W-2 check
|
|
2344
|
+
// one level down, and the run wall-halts anyway. u-msmt91t3 paid for exactly that
|
|
2345
|
+
// and called the result `step-red`. So the gate declines to FUND it.
|
|
2346
|
+
//
|
|
2347
|
+
// Nothing is routed here and no readout is duplicated: declining simply lets the
|
|
2348
|
+
// stop fall through to the `step-variance` terminal below, which is the one site
|
|
2349
|
+
// that mints the wall-halt package (record + F11-consistent escalation + levers).
|
|
2350
|
+
// One site, one spelling — a second copy here is how two instruments come to
|
|
2351
|
+
// disagree about one event.
|
|
2352
|
+
//
|
|
2353
|
+
// TRIGGER-AGNOSTIC (hamr: "extend"). It first covered the variance trigger only,
|
|
2354
|
+
// the one measured on u-msmt91t3; the ladder's exhaustion and the stall fuse reach
|
|
2355
|
+
// this gate past the deadline too, and a doomed cycle costs the same money whichever
|
|
2356
|
+
// instrument named the stop. Each trigger then lands on its own honest terminal
|
|
2357
|
+
// below — the wall for the two that would otherwise mint `step-red`, its own name
|
|
2358
|
+
// for the stall (which already rides out as `step-stalled` and must, because run.js
|
|
2359
|
+
// keys the F44 spend FLOOR on that outcome).
|
|
2360
|
+
//
|
|
2361
|
+
// The latch is NOT consumed — no grant was spent, because no replan was bought.
|
|
2362
|
+
// With time on the clock this is byte-identical to today, for every trigger.
|
|
2363
|
+
const wallStopsReplan = clock.expired();
|
|
2364
|
+
if (!wallStopsReplan && (!replanned || grantExtraReplan) && replanTrigger && remainingUsd() > MONEY_MIN) {
|
|
2015
2365
|
if (grantExtraReplan) varianceGrantUsed = true;
|
|
2016
2366
|
replanned = true;
|
|
2017
2367
|
replans += 1;
|
|
@@ -2110,6 +2460,38 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2110
2460
|
skipCount = 0;
|
|
2111
2461
|
continue;
|
|
2112
2462
|
}
|
|
2463
|
+
/**
|
|
2464
|
+
* W-2's STEP-LOOP terminal, and the ONE place it is spelled. Reached whenever a
|
|
2465
|
+
* trigger that would otherwise mint `step-red:<id>` lands with the deadline already
|
|
2466
|
+
* passed: the meter's stop (F85's variance) and the ladder's (attempt exhaustion
|
|
2467
|
+
* with money still on the table). Both are the same event wearing two instruments'
|
|
2468
|
+
* names — the clock is what ended the run — and `step-red` is a CAPABILITY label,
|
|
2469
|
+
* so minting it there files a governance stop as "the work failed". u-msmt91t3 is
|
|
2470
|
+
* the measured instance: `step-red` 9.9 seconds after the run's own `wall-bounded`.
|
|
2471
|
+
*
|
|
2472
|
+
* One site, one spelling, because the record, the F11-consistent escalation and the
|
|
2473
|
+
* lever list must be identical wherever the clock stops a step — a second copy per
|
|
2474
|
+
* trigger is how two instruments come to disagree about one event. The instrument
|
|
2475
|
+
* that named the stop FIRST is not discarded: it rides in the detail, so the human
|
|
2476
|
+
* reads why the step was stopped as well as which lever fits.
|
|
2477
|
+
*
|
|
2478
|
+
* `stepWallStop` is never consulted here by construction: that record is written
|
|
2479
|
+
* only by the head-of-attempt check, and both triggers above are read BEFORE it, so
|
|
2480
|
+
* a step either of them stopped never reached it.
|
|
2481
|
+
* @param {string} stoppedBy the instrument that named the stop first
|
|
2482
|
+
*/
|
|
2483
|
+
const wallHaltTerminal = (stoppedBy) => {
|
|
2484
|
+
emitWallHalt({ cutMidCall: false, phase: `step:${step.id}`, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
2485
|
+
emit('escalation', {
|
|
2486
|
+
category: 'wall-halt', decisionReady: true, phase: `step:${step.id}`,
|
|
2487
|
+
decision: `The run reached its wall-clock cap while step "${step.id}" was running. Time ran out, not capability — the verdict the run already has stands.`,
|
|
2488
|
+
options: WALL_OPTIONS,
|
|
2489
|
+
detail: `requested ${clock.requestedMs}ms, elapsed ${clock.elapsedMs()}ms. `
|
|
2490
|
+
+ `${stoppedBy}: ${lastEscalation?.detail ?? '(none)'} `
|
|
2491
|
+
+ `Progress trend: ${stopTrend.trend} — ${stopTrend.reading}; ${stopTrend.lever}.`,
|
|
2492
|
+
});
|
|
2493
|
+
return 'wall-halt';
|
|
2494
|
+
};
|
|
2113
2495
|
// F64 — the wall stopped this attempt from INSIDE a provider call (the derived
|
|
2114
2496
|
// call timeout is the deadline in the provider's own coin). ralph already
|
|
2115
2497
|
// emitted the escalation under this category, so the outcome and the record
|
|
@@ -2132,7 +2514,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2132
2514
|
// stays a step-red (the stop is a result).
|
|
2133
2515
|
if (cat === 'cap-halt') {
|
|
2134
2516
|
planExecuted();
|
|
2135
|
-
if (remainingUsd() > MONEY_MIN)
|
|
2517
|
+
if (remainingUsd() > MONEY_MIN) {
|
|
2518
|
+
// W-2 EXTENDED to the ladder's stop (hamr: "extend"). This arm is the sibling of
|
|
2519
|
+
// the variance terminal below and carries the identical defect: with money still
|
|
2520
|
+
// on the table it minted `step-red` whatever the clock said, so a step the
|
|
2521
|
+
// deadline ended was filed as a capability failure. The wallet is re-read (it
|
|
2522
|
+
// always was); the clock is re-read too, and it is read FIRST inside this arm
|
|
2523
|
+
// only — a DRAINED wallet keeps its own `cap-halt` terminal below, because money
|
|
2524
|
+
// is the allowance actually blocking that run and its levers are the ones the
|
|
2525
|
+
// human needs (a wall-halt readout there would offer "raise maxWallMs and rerun"
|
|
2526
|
+
// into an immediate money cut).
|
|
2527
|
+
if (clock.expired()) return wallHaltTerminal('The ladder ended the step first');
|
|
2528
|
+
return `step-red:${step.id}`;
|
|
2529
|
+
}
|
|
2136
2530
|
// v1.46 §2: the money cut is decision-ready HERE too, and for the same reason
|
|
2137
2531
|
// W-2 gave the wall a record at every site it stops a run — a category that
|
|
2138
2532
|
// hands the human a readout at one seam and silence at another is the readout
|
|
@@ -2148,8 +2542,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2148
2542
|
// upward would mint an outcome run.js and the ledger's class table do not know,
|
|
2149
2543
|
// and an unmapped category is counted as a library bug (ledger.js) when this is a
|
|
2150
2544
|
// planning story. The escalation ralph already emitted carries the real detail.
|
|
2545
|
+
//
|
|
2546
|
+
// W-2 at the meter's own terminal. A `step-variance` stop that lands PAST the
|
|
2547
|
+
// deadline is a TIME stop wearing the meter's name — the clock is what ended the
|
|
2548
|
+
// run, and hamr's ruling names that stop: *"when time is up, keep the grade we
|
|
2549
|
+
// already have and stop"*. Its cap-halt sibling directly above already re-reads its
|
|
2550
|
+
// own governor (the wallet) before minting a terminal; this branch did not, and on
|
|
2551
|
+
// u-msmt91t3 it minted `step-red` 9.9 seconds AFTER the run's own `wall-bounded`
|
|
2552
|
+
// record — a governance stop filed as a capability read, and (the resume gate's
|
|
2553
|
+
// half of the same defect) a checkpoint filed as an answer.
|
|
2554
|
+
//
|
|
2555
|
+
// The clock is read HERE, with nothing in flight, exactly as the three other
|
|
2556
|
+
// terminal sites read it. With time still on the clock this is byte-identical to
|
|
2557
|
+
// what it was: the step-red is the designed stop, and the stop is the result.
|
|
2151
2558
|
if (cat === 'step-variance') {
|
|
2152
2559
|
planExecuted();
|
|
2560
|
+
if (clock.expired()) return wallHaltTerminal('The meter stopped the step first');
|
|
2153
2561
|
return `step-red:${step.id}`;
|
|
2154
2562
|
}
|
|
2155
2563
|
// Any OTHER terminal escalation category is NOT a capability failure: a
|