bareloop 0.8.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 +421 -0
- package/README.md +10 -3
- package/bareloop.context.md +198 -17
- 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/index.js +72 -0
- package/src/job.js +48 -4
- package/src/kinds.js +1354 -0
- package/src/ledger.js +13 -0
- package/src/plan.js +26 -1
- package/src/planrun.js +239 -16
- package/src/ralph.js +19 -4
- package/src/reuse.js +68 -7
- package/src/root.js +8 -1
- package/src/run.js +10 -3
- package/src/text.js +120 -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/index.d.ts +9 -1
- package/types/job.d.ts +1 -0
- package/types/kinds.d.ts +343 -0
- package/types/plan.d.ts +18 -0
- package/types/planrun.d.ts +10 -3
- package/types/ralph.d.ts +1 -0
- package/types/run.d.ts +10 -2
- package/types/text.d.ts +84 -0
- package/types/workbranch.d.ts +102 -0
package/src/ledger.js
CHANGED
|
@@ -79,6 +79,14 @@ const EXCLUDED_ESCALATIONS = new Set([
|
|
|
79
79
|
'smoke-red', // already counted via primitive-smoke
|
|
80
80
|
'hitl-close', // by design: a human is the close
|
|
81
81
|
'close-unsupported', // honest refusal, by design
|
|
82
|
+
// Close authoring (M4, D13) — the interview refusing a job we cannot close: a
|
|
83
|
+
// genre we do not run, a job with no seed to measure against, a locked kind.
|
|
84
|
+
// Honest refusal, exactly like `close-unsupported` above, and excluded for a
|
|
85
|
+
// SECOND reason that matters more: the demand is already counted once, as the
|
|
86
|
+
// `request-red` the same refusal emits with `lib: 'bareloop'` stamped at its
|
|
87
|
+
// emit site. Counting the escalation too would double every refusal in the one
|
|
88
|
+
// number the admission path exists to measure.
|
|
89
|
+
'close-unauthorable',
|
|
82
90
|
// Layer 3 (D2 split): the bridge load gate refused a wrong-KIND recipe at the door.
|
|
83
91
|
// That is the mechanism WORKING — every recipe from a different day is a candidate for
|
|
84
92
|
// refusal, and the run stopped having spent nothing. Filing it upstream would aim a bug
|
|
@@ -99,6 +107,11 @@ const EXCLUDED_ESCALATIONS = new Set([
|
|
|
99
107
|
// in the registry. Both are the operator's own state speaking (a stale signature, an
|
|
100
108
|
// edited registry), and both stop having run nothing.
|
|
101
109
|
'resume-red',
|
|
110
|
+
// v1.57 §3 — the work branch could not be created: the patient is not a git checkout,
|
|
111
|
+
// or its branch namespace is too crowded to find a free name. The operator's own state
|
|
112
|
+
// speaking, exactly like `registry-red` below, and the run stopped having spent nothing.
|
|
113
|
+
// Filing it upstream would aim a bug report at a library for a patient nobody prepared.
|
|
114
|
+
'branch-red',
|
|
102
115
|
'registry-red', // the operator-supplied registry path does not exist — a typo, not a bug
|
|
103
116
|
'envelope-red', // the envelope does not compose with the signed spec — operator input
|
|
104
117
|
// the reuse runner also STOPS on the three answers no further try could change, and
|
package/src/plan.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// plain parsed data; every failure is a named {code, path, detail} red.
|
|
26
26
|
|
|
27
27
|
import { TOOL_MENU, LOCKED_TOOLS, WRITE_VERBS, checkMenu } from './job.js';
|
|
28
|
+
import { declaredStages } from './declaredclose.js';
|
|
28
29
|
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
|
|
29
30
|
|
|
30
31
|
/** the closed exit menu (PRD v1.12 §3 + decision 1's `check-passes`): the
|
|
@@ -170,6 +171,30 @@ export function stageClose(close) {
|
|
|
170
171
|
return [{ name: 'close', cmd: close.cmd, expect: close.expect, judged: close.judged, gapKeep: close.gapKeep }];
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* THE ONE STAGING, one level up: a job's close stages, from EITHER of the two
|
|
176
|
+
* fields a spec may carry.
|
|
177
|
+
*
|
|
178
|
+
* `stageClose` reads a command close; `declaredStages` reads an authored
|
|
179
|
+
* declaration (M4). A spec declares exactly one of them — the job validator reds
|
|
180
|
+
* `close-duplicated` on both — so this is a selection, never a merge.
|
|
181
|
+
*
|
|
182
|
+
* Every consumer calls THIS rather than `stageClose` directly, for the reason
|
|
183
|
+
* W4 wrote `stageClose` in the first place: the drafting prompt, the plan
|
|
184
|
+
* validator, the runner, the bridge registry and the reuse envelope must all see
|
|
185
|
+
* the same stage list, and a consumer that reads only `close` sees a declared
|
|
186
|
+
* job as having no close at all — an empty check menu offered to a drafter whose
|
|
187
|
+
* plan the validator would then red for referencing a stage nobody offered.
|
|
188
|
+
* @param {any} job the job spec, any shape
|
|
189
|
+
* @returns {any[]|null} the stage list, or null when the job names no close
|
|
190
|
+
*/
|
|
191
|
+
export function closeStagesOf(job) {
|
|
192
|
+
if (!isObj(job)) return null;
|
|
193
|
+
const declared = declaredStages(job.closeDecl);
|
|
194
|
+
if (declared) return declared;
|
|
195
|
+
return stageClose(job.close);
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
|
|
174
199
|
|
|
175
200
|
/**
|
|
@@ -235,7 +260,7 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, seedRed,
|
|
|
235
260
|
// (gold/rubric/hitl) stages nothing, so it offers nothing — the runner
|
|
236
261
|
// escalates `close-unsupported` on the same null, and an empty menu is already
|
|
237
262
|
// the honest reading here (every check-passes becomes a check-unknown).
|
|
238
|
-
const checkNames = checkMenu(
|
|
263
|
+
const checkNames = checkMenu(closeStagesOf(spec) ?? []).map((m) => m.name);
|
|
239
264
|
// The offered scope menu. A caller-supplied menu must be the same one the
|
|
240
265
|
// prompt enumerated; with none, derive from the signed fence — fail-CLOSED,
|
|
241
266
|
// so omitting the option narrows the agent's choices and never widens them.
|
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 } 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');
|
|
@@ -306,7 +309,7 @@ export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes,
|
|
|
306
309
|
// executes and `validatePlan` accepts against. Reading `job.close` raw here left
|
|
307
310
|
// the legacy object form offering nothing while the runner preflighted a stage.
|
|
308
311
|
// (`?? []`: a close naming no command stages nothing, so it offers nothing)
|
|
309
|
-
const closeStages =
|
|
312
|
+
const closeStages = closeStagesOf(job) ?? [];
|
|
310
313
|
const checkNames = checkMenu(closeStages).map((m) => m.name);
|
|
311
314
|
const doc = `DRAFT-PLAN
|
|
312
315
|
You are planning how to accomplish a goal in a repository, as an ordered list of bounded
|
|
@@ -509,12 +512,18 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
509
512
|
* default; a caller supplies this to drive time deterministically (the same seam `createClock`
|
|
510
513
|
* already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
|
|
511
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.
|
|
512
521
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
513
522
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
514
|
-
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
515
|
-
* `step-red:<id>`
|
|
523
|
+
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
524
|
+
* 'step-stalled' | `step-red:<id>`
|
|
516
525
|
*/
|
|
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 }) {
|
|
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 }) {
|
|
518
527
|
workdir = resolve(workdir);
|
|
519
528
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
520
529
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
@@ -550,14 +559,17 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
550
559
|
// A STAGED close (PRD v1.28) is an ordered list of command stages and is the
|
|
551
560
|
// go-forward shape; the object form survives only for the declared-but-locked
|
|
552
561
|
// verdict classes (gold/rubric/hitl), which name no command to run.
|
|
553
|
-
// W4: the staging itself lives in ONE place (`
|
|
562
|
+
// W4: the staging itself lives in ONE place (`closeStagesOf`), shared with
|
|
554
563
|
// `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
|
-
|
|
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);
|
|
557
569
|
if (!stagedClose) {
|
|
558
570
|
emit('escalation', {
|
|
559
571
|
category: 'close-unsupported', decisionReady: true,
|
|
560
|
-
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).`,
|
|
561
573
|
options: ['restate the close as a predicate', 'wait for the verdict-classes rung'],
|
|
562
574
|
});
|
|
563
575
|
return 'close-unsupported';
|
|
@@ -738,6 +750,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
738
750
|
];
|
|
739
751
|
|
|
740
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));
|
|
741
769
|
/** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
|
|
742
770
|
let closeStage;
|
|
743
771
|
/** @type {any} the LAST close verdict itself (W-2). A wall stop KEEPS the grade
|
|
@@ -795,16 +823,172 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
795
823
|
};
|
|
796
824
|
/** the close, as ONE verdict: every stage in order, first red wins */
|
|
797
825
|
const judgeClose = async () => {
|
|
798
|
-
const v = await
|
|
826
|
+
const v = await runCloseStages(stagedClose);
|
|
799
827
|
closeStage = v.stage;
|
|
800
828
|
lastCloseVerdict = v;
|
|
801
829
|
// Only a RED grade is a reading. A `satisfied` ends the run and a close FAULT
|
|
802
830
|
// rendered no judgment at all (CLOSE_FAULTS) — folding either into the series
|
|
803
831
|
// would put a non-number where the instrument expects a graded one.
|
|
804
|
-
|
|
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));
|
|
805
837
|
return v;
|
|
806
838
|
};
|
|
807
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
|
+
|
|
808
992
|
// ── 0a. close precheck (close-first, F17): already-green is a DISTINCT
|
|
809
993
|
// record, zero tokens; a forbidden-zone verdict escalates before any spend
|
|
810
994
|
const pre = await judgeClose();
|
|
@@ -838,7 +1022,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
838
1022
|
: {}),
|
|
839
1023
|
});
|
|
840
1024
|
for (const m of menu) {
|
|
841
|
-
const v = await
|
|
1025
|
+
const v = await runCloseStages(m.run);
|
|
842
1026
|
if (v.verdict === 'needs_revision') {
|
|
843
1027
|
seedRed.push(m.name);
|
|
844
1028
|
// …and the preflight grade is a READING, for the same reason the precheck one
|
|
@@ -856,7 +1040,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
856
1040
|
// handed to a human and to the replanner, which is the exact class of false
|
|
857
1041
|
// story this change exists to remove. Asked of the reader's own books, so
|
|
858
1042
|
// there is no second record of what has been graded.
|
|
859
|
-
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record(
|
|
1043
|
+
if (!runTrend.report().stages.some((s) => s.stage === v.stage)) runTrend.record(closeGrade(v));
|
|
860
1044
|
}
|
|
861
1045
|
emit('check-preflight', { name: m.name, verdict: v.verdict, ...(m.run.length > 1 ? { chain: m.run.map((s2) => s2.name) } : {}) });
|
|
862
1046
|
const f = Object.hasOwn(CLOSE_FAULTS, v.verdict) ? CLOSE_FAULTS[v.verdict] : undefined;
|
|
@@ -866,6 +1050,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
866
1050
|
}
|
|
867
1051
|
}
|
|
868
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
|
+
|
|
869
1062
|
const lc = new LiteCtx({ root: workdir });
|
|
870
1063
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
871
1064
|
const fencePrefixes = job.writeScope.map((/** @type {string} */ g) => resolve(workdir, globToPrefix(g)));
|
|
@@ -909,7 +1102,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
909
1102
|
// derivation, not a live path; the reachable guard is the validator's, and
|
|
910
1103
|
// that one is mutation-covered.
|
|
911
1104
|
if (!chain) return { pass: false, fault: 'failed', gap: `no offered close stage named "${name}"` };
|
|
912
|
-
const v = await
|
|
1105
|
+
const v = await runCloseStages(chain);
|
|
913
1106
|
emit('check-run', { name, verdict: v.verdict, ...(v.stage && v.stage !== name ? { stage: v.stage } : {}), ...(v.exitCode !== undefined ? { exitCode: v.exitCode } : {}) });
|
|
914
1107
|
if (v.verdict === 'satisfied') return { pass: true };
|
|
915
1108
|
if (v.verdict === 'needs_revision') {
|
|
@@ -927,7 +1120,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
927
1120
|
// step grade donates a null. Measured on u-msh70zla's own archived gaps: raw
|
|
928
1121
|
// reads `typecheck 24 → 15 → 14`, wrapped reads nothing at all. Same seam the
|
|
929
1122
|
// Layer R note below already calls out for a check's `^`-anchored gapKeep.
|
|
930
|
-
runTrend.record(
|
|
1123
|
+
runTrend.record(closeGrade(v));
|
|
931
1124
|
return { pass: false, gap: v.gap };
|
|
932
1125
|
}
|
|
933
1126
|
return { pass: false, fault: v.verdict, gap: v.detail };
|
|
@@ -955,6 +1148,27 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
955
1148
|
* the turn-allowance note below.
|
|
956
1149
|
*/
|
|
957
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
|
+
}
|
|
958
1172
|
/**
|
|
959
1173
|
* The gate's LLM-turn allowance THROUGH iteration `i` — the old expression
|
|
960
1174
|
* `attemptRounds * (attempts + 1)` with the iteration count made explicit
|
|
@@ -1388,6 +1602,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1388
1602
|
// and those are different diagnoses with different remedies (F45/F48: a
|
|
1389
1603
|
// casualty is never evidence, and a governance stop is never a casualty).
|
|
1390
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']],
|
|
1391
1613
|
};
|
|
1392
1614
|
const [decision, options] = (Object.hasOwn(DECIDE, category) ? DECIDE[category] : undefined)
|
|
1393
1615
|
?? [`The ${phase} call failed (${category}) — no result exists.`, ['retry the run', 'fix the provider binding', 'abandon the run']];
|
|
@@ -1398,7 +1620,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1398
1620
|
// `step-stalled` rides out as ITSELF, not as provider-red. Naming the
|
|
1399
1621
|
// escalation while returning a casualty label would launder a governance stop
|
|
1400
1622
|
// into transport noise at exactly the layer the readout reads.
|
|
1401
|
-
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';
|
|
1402
1625
|
};
|
|
1403
1626
|
|
|
1404
1627
|
// ── 1. SCOUT — read-only by construction: the write-class verbs are simply
|
package/src/ralph.js
CHANGED
|
@@ -285,6 +285,21 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The staged gap's header — WHICH wall the close stopped at, in front of that
|
|
290
|
+
* wall's own output. A worker handed three stages' worth of output cannot tell
|
|
291
|
+
* which one it must satisfy, and `src/trend.js` parses this exact line to bucket
|
|
292
|
+
* a grade per stage (its `STAGE_HEADER`).
|
|
293
|
+
*
|
|
294
|
+
* Exported for the one reason `GAP_KEEP_TRIM_MARKER` is: a SECOND executor now
|
|
295
|
+
* renders staged gaps (`src/declaredclose.js`, the declared close), and two
|
|
296
|
+
* spellings of this line would be two instruments — the reader would silently
|
|
297
|
+
* stop bucketing whichever one drifted. Behaviour here is unchanged; this is the
|
|
298
|
+
* same template, named.
|
|
299
|
+
* @param {string} name @param {string} gap
|
|
300
|
+
*/
|
|
301
|
+
export const stageGap = (name, gap) => `close stage "${name}" failed:\n${gap}`;
|
|
302
|
+
|
|
288
303
|
/**
|
|
289
304
|
* Run a STAGED close (PRD v1.28) — an ordered list of named command stages, run
|
|
290
305
|
* in order until one renders a verdict that is not `satisfied`. The return is
|
|
@@ -310,7 +325,7 @@ export async function runClose(close, redact = (s) => s, { timeoutMs = 120_000,
|
|
|
310
325
|
* @param {{timeoutMs?: number, cwd?: string}} [opts]
|
|
311
326
|
* @returns {Promise<any>} runClose's verdict shape, plus `stage` and `stages`
|
|
312
327
|
*/
|
|
313
|
-
export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } = {}) {
|
|
328
|
+
export async function runStages(stages, redact = (s) => s, /** @type {{timeoutMs?: number, cwd?: string}} */ { timeoutMs, cwd } = {}) {
|
|
314
329
|
const ran = [];
|
|
315
330
|
let last;
|
|
316
331
|
for (const st of stages) {
|
|
@@ -326,9 +341,9 @@ export async function runStages(stages, redact = (s) => s, { timeoutMs, cwd } =
|
|
|
326
341
|
...r,
|
|
327
342
|
stage: st.name,
|
|
328
343
|
stages: ran,
|
|
329
|
-
// The gap says WHICH wall, then the wall's own output
|
|
330
|
-
//
|
|
331
|
-
...(r.gap ? { gap:
|
|
344
|
+
// The gap says WHICH wall, then the wall's own output (`stageGap`, the
|
|
345
|
+
// one spelling — the declared executor renders the same header).
|
|
346
|
+
...(r.gap ? { gap: stageGap(st.name, r.gap) } : {}),
|
|
332
347
|
};
|
|
333
348
|
}
|
|
334
349
|
}
|
package/src/reuse.js
CHANGED
|
@@ -48,7 +48,7 @@ import { createHash } from 'node:crypto';
|
|
|
48
48
|
import { createRequire } from 'node:module';
|
|
49
49
|
import { loadRegistry, saveBridge, appendGreen, appendRed, mintBridge, registryExists } from './bridges.js';
|
|
50
50
|
import { renderListing, selectionPrompt } from './selection.js';
|
|
51
|
-
import {
|
|
51
|
+
import { closeStagesOf } from './plan.js';
|
|
52
52
|
import { MIN_WALL_MS, jobSpecHash } from './job.js';
|
|
53
53
|
import { isObj, isNonEmptyString } from './validate.js';
|
|
54
54
|
import { readGrade } from './trend.js';
|
|
@@ -498,6 +498,30 @@ function readStepCheckpoint(seen) {
|
|
|
498
498
|
};
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
/**
|
|
502
|
+
* The WORK BRANCH the dead leg created (PRD v1.57 §3), read off its own `work-branch`
|
|
503
|
+
* record — the one place the runner writes it down.
|
|
504
|
+
*
|
|
505
|
+
* It is read rather than re-derived, and that is the whole point. `workBranchName` is
|
|
506
|
+
* deterministic from the signed spec, so a restart computing it fresh would get the very
|
|
507
|
+
* name its predecessor already took and the collision walk would hand it
|
|
508
|
+
* `<name>-2` — a brand-new branch standing beside the work the resume exists to
|
|
509
|
+
* continue. The declared-fold precedent (`priorSpentUsd`, `priorReplans`): what the
|
|
510
|
+
* chain already did is DECLARED on the spine and inherited, never recomputed.
|
|
511
|
+
*
|
|
512
|
+
* The LAST record wins. A leg emits exactly one, but a window can reach back through an
|
|
513
|
+
* abandoned attempt of the same try, and the newest is the branch the work is actually
|
|
514
|
+
* sitting on.
|
|
515
|
+
* @param {any[]} seen the window's own events, in file order
|
|
516
|
+
* @returns {string|null} null on a spine older than the record, or a kill before 0c
|
|
517
|
+
*/
|
|
518
|
+
function readWorkBranch(seen) {
|
|
519
|
+
for (let i = seen.length - 1; i >= 0; i -= 1) {
|
|
520
|
+
if (seen[i].type === 'work-branch' && isNonEmptyString(seen[i].branch)) return String(seen[i].branch);
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
501
525
|
/**
|
|
502
526
|
* The dead leg's CLOSE GRADES, in order — the trend baselines a resumed leg
|
|
503
527
|
* inherits (PRD v1.46 §3, and hamr's approved #2: *a resumed run's trend judges the
|
|
@@ -842,6 +866,17 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
842
866
|
// restart already re-graded that tree. Counting both would enter the same
|
|
843
867
|
// reading twice and flatten a chain that moved.
|
|
844
868
|
grades: readGradeSeed(open.seen),
|
|
869
|
+
// the WORK BRANCH this leg's uncommitted work is sitting on (v1.57 §3). Read
|
|
870
|
+
// from THIS window first and then from the newest abandoned window of the same
|
|
871
|
+
// try, on the checkpoint's own rule and for the checkpoint's own reason: the
|
|
872
|
+
// branch — like the plan — is a fact about the TREE, and the tree survives an
|
|
873
|
+
// attempt that died before recording anything of its own. Null is the honest
|
|
874
|
+
// answer for a leg killed before 0c (or a spine older than the record); the
|
|
875
|
+
// resume then takes the cold path and creates one, which is correct, because a
|
|
876
|
+
// leg that never branched has no work to strand.
|
|
877
|
+
branch: readWorkBranch(open.seen)
|
|
878
|
+
?? abandoned.filter((w) => w.n === open.n).map((w) => readWorkBranch(w.seen)).filter(Boolean).at(-1)
|
|
879
|
+
?? null,
|
|
845
880
|
// the REPLAN ledger, folded like the money one line below and deliberately
|
|
846
881
|
// NOT like the grades one line above. A grade seed feeds a READOUT and its
|
|
847
882
|
// documented shortening chain can only under-claim a direction; this feeds a
|
|
@@ -1170,8 +1205,14 @@ export async function runReuse(opts) {
|
|
|
1170
1205
|
* much of the try's allowance is gone; this says how much of its WORK is done, so the
|
|
1171
1206
|
* restart re-pays for neither the scout, the draft, nor a finished step. Null is the
|
|
1172
1207
|
* scout/draft death, where nothing paid is re-payable and the try simply restarts.
|
|
1208
|
+
* @param {string|null} [branch] RESUME (v1.57 §3) — the WORK BRANCH that attempt's work
|
|
1209
|
+
* is sitting on (`readResume`'s `restart.branch`). The name is derived from the SIGNED
|
|
1210
|
+
* spec and is therefore the same on every leg, so a restart that recomputed it would
|
|
1211
|
+
* collide with its own predecessor and be handed a fresh `-2` beside the work it came
|
|
1212
|
+
* back for. Declared and inherited, exactly like the money and the replan ledger. Null
|
|
1213
|
+
* is the cold path, and also the leg killed before it ever branched.
|
|
1173
1214
|
* @returns {Promise<any>} the try row */
|
|
1174
|
-
const runTry = async (bridge, n, prior = NO_PRIOR, seed = null) => {
|
|
1215
|
+
const runTry = async (bridge, n, prior = NO_PRIOR, seed = null, branch = null) => {
|
|
1175
1216
|
const mode = bridge ? 'bridge' : 'cold';
|
|
1176
1217
|
const name = bridge ? bridge.name : null;
|
|
1177
1218
|
const resumed = prior.spentUsd > 0 || prior.wallMs > 0;
|
|
@@ -1231,6 +1272,10 @@ export async function runReuse(opts) {
|
|
|
1231
1272
|
// a bound the operator signed, widened by a kill (PRD v1.12).
|
|
1232
1273
|
...(prior.replans > 0 ? { resumeReplans: { count: prior.replans, grantUsed: prior.replanGrantUsed } } : {}),
|
|
1233
1274
|
...(seed ? { resumeSeed: seed } : {}),
|
|
1275
|
+
// …and WHERE its work is. Same omission class as the three folds above, with its
|
|
1276
|
+
// own cost: a branch that never arrives is not a wrong number but a SECOND branch,
|
|
1277
|
+
// and the leg would carry on beside the tree it was resumed to continue.
|
|
1278
|
+
...(branch ? { resumeBranch: branch } : {}),
|
|
1234
1279
|
bridge,
|
|
1235
1280
|
});
|
|
1236
1281
|
} catch (e) {
|
|
@@ -1290,7 +1335,7 @@ export async function runReuse(opts) {
|
|
|
1290
1335
|
name: name ?? job.job,
|
|
1291
1336
|
goal: job.goal,
|
|
1292
1337
|
specHash: trySpecHash,
|
|
1293
|
-
closeStageNames: (
|
|
1338
|
+
closeStageNames: (closeStagesOf(job) ?? []).map((/** @type {any} */ s) => s.name),
|
|
1294
1339
|
toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
|
|
1295
1340
|
};
|
|
1296
1341
|
row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
|
|
@@ -1423,6 +1468,19 @@ export async function runReuse(opts) {
|
|
|
1423
1468
|
decision: 'The plan flow cannot execute this job\'s close — it runs commands whose exit codes are truth. Nothing was judged, and no workflow can be graded by a close that will not run, so the next try and the cold leg would refuse identically for $0 apiece.',
|
|
1424
1469
|
options: ['restate the close as a staged (or single predicate) close — a spec edit, whose new hash needs re-approval', 'wait for the verdict-classes rung'],
|
|
1425
1470
|
},
|
|
1471
|
+
'branch-red': {
|
|
1472
|
+
// NOT re-filed, for `interpreter-red`'s reason plus one of its own: the runner
|
|
1473
|
+
// already escalated it at the fault's own site, and the fault is the OPERATOR's
|
|
1474
|
+
// state (a patient that is not a git checkout, a crowded branch namespace),
|
|
1475
|
+
// never a library failing — the ledger excludes it for exactly that.
|
|
1476
|
+
refile: false,
|
|
1477
|
+
decision: 'The run could not create its own work branch, and no job runs on the branch it was handed (PRD v1.57 §3). That is a property of the PATIENT, not of any stored workflow: every further try and the cold leg would refuse identically, for $0 apiece.',
|
|
1478
|
+
options: [
|
|
1479
|
+
'make the patient a git checkout with at least one commit',
|
|
1480
|
+
'free the work-branch name (delete or rename the stale branches this run collided with)',
|
|
1481
|
+
'abandon the run',
|
|
1482
|
+
],
|
|
1483
|
+
},
|
|
1426
1484
|
'interpreter-red': {
|
|
1427
1485
|
// NOT re-filed as an escalation. `interpreter-red` is one of the categories the
|
|
1428
1486
|
// ledger CLASSIFIES — it aims a runtime-red at a library — and runJob already
|
|
@@ -1522,7 +1580,10 @@ export async function runReuse(opts) {
|
|
|
1522
1580
|
// `gradesInherited` is a COUNT, and only when there is one to state: the seam
|
|
1523
1581
|
// carries stage names and numbers, never a close byte — the spine is append-only,
|
|
1524
1582
|
// so a gap that crosses here crosses for good.
|
|
1525
|
-
restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {})
|
|
1583
|
+
restart: rs ? { n: rs.n, mode: rs.mode, bridge: rs.bridge ?? null, priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, remainingCapUsd, remainingWallMs, resumedAt: rs.seed ? { phase: rs.seed.phase, stepsDone: rs.seed.completedSteps.length, stepsPlanned: rs.seed.plan?.steps?.length ?? null } : null, ...(prior.grades.length ? { gradesInherited: prior.grades.length } : {}),
|
|
1584
|
+
// WHERE it comes back to (v1.57 §3), stated before anything is spent rather than
|
|
1585
|
+
// discovered from the restarted leg's own `work-branch` record afterwards
|
|
1586
|
+
...(rs.branch ? { branch: rs.branch } : {}) } : null,
|
|
1526
1587
|
nextTry: rs ? null : startN,
|
|
1527
1588
|
});
|
|
1528
1589
|
|
|
@@ -1560,7 +1621,7 @@ export async function runReuse(opts) {
|
|
|
1560
1621
|
if (rs.mode === 'cold') {
|
|
1561
1622
|
// every bridge try was already spent before the cold leg started: the loop below
|
|
1562
1623
|
// has nothing left to authorize, and the restart IS the cold attempt
|
|
1563
|
-
coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null };
|
|
1624
|
+
coldRestart = { n: typeof rs.n === 'number' ? rs.n : tries.length + 1, prior, seed: rs.seed ?? null, branch: rs.branch ?? null };
|
|
1564
1625
|
startN = resolved.bridgeTries + 1;
|
|
1565
1626
|
} else {
|
|
1566
1627
|
const entry = loadRegistry(registryDir).bridges.find((/** @type {any} */ b) => b.name === rs.bridge);
|
|
@@ -1576,7 +1637,7 @@ export async function runReuse(opts) {
|
|
|
1576
1637
|
});
|
|
1577
1638
|
}
|
|
1578
1639
|
tried.add(entry.name);
|
|
1579
|
-
const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null));
|
|
1640
|
+
const stop = afterTry(await runTry(entry, rs.n, prior, rs.seed ?? null, rs.branch ?? null));
|
|
1580
1641
|
if (stop) return stop;
|
|
1581
1642
|
startN = rs.n + 1;
|
|
1582
1643
|
}
|
|
@@ -1644,7 +1705,7 @@ export async function runReuse(opts) {
|
|
|
1644
1705
|
// the verdict that attempt actually earned.
|
|
1645
1706
|
const inheritedCold = coldRestart ? null : tries.find((t) => t.mode === 'cold' && t.inherited) ?? null;
|
|
1646
1707
|
const cold = coldRestart
|
|
1647
|
-
? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed)
|
|
1708
|
+
? await runTry(null, coldRestart.n, coldRestart.prior, coldRestart.seed, coldRestart.branch)
|
|
1648
1709
|
: inheritedCold ?? await runTry(null, tries.length + 1);
|
|
1649
1710
|
const coldStop = afterTry(cold);
|
|
1650
1711
|
if (coldStop) return coldStop;
|