bareloop 0.9.0 → 0.11.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 +555 -0
- package/README.md +111 -117
- package/bareloop.context.md +314 -24
- package/package.json +3 -3
- package/src/authorflow.js +300 -35
- package/src/authoring.js +290 -33
- package/src/authorjob.js +59 -7
- package/src/authorscout.js +83 -10
- package/src/bridges.js +15 -2
- package/src/clock.js +21 -3
- package/src/declaredclose.js +88 -11
- package/src/index.js +11 -1
- package/src/job.js +7 -2
- package/src/kinds.js +292 -20
- package/src/ledger.js +29 -2
- package/src/planrun.js +404 -29
- package/src/ralph.js +14 -0
- package/src/reuse.js +247 -9
- package/src/root.js +1 -1
- package/src/run.js +12 -5
- package/src/selection.js +1 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +135 -17
- package/types/authoring.d.ts +161 -35
- package/types/authorjob.d.ts +11 -2
- package/types/authorscout.d.ts +22 -3
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +64 -6
- package/types/index.d.ts +4 -4
- package/types/job.d.ts +5 -0
- package/types/kinds.d.ts +134 -10
- package/types/planrun.d.ts +61 -2
- package/types/reuse.d.ts +75 -1
- package/types/run.d.ts +12 -2
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/src/planrun.js
CHANGED
|
@@ -33,8 +33,11 @@ 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 {
|
|
36
|
+
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED } from './declaredclose.js';
|
|
37
|
+
import {
|
|
38
|
+
seedAtHead, seedListing, changedSet, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES,
|
|
39
|
+
HUMAN_KIND, HUMAN_DECISIONS, normalizeHumanRuling,
|
|
40
|
+
} from './kinds.js';
|
|
38
41
|
import { workBranchName, prepareWorkBranch } from './workbranch.js';
|
|
39
42
|
|
|
40
43
|
const require = createRequire(import.meta.url);
|
|
@@ -65,6 +68,11 @@ const NATIVE_READ_STRATEGY = '\nINTERFACE LIMIT: on this surface a whole-file re
|
|
|
65
68
|
const SCOUT_MIN_BYTES = 200;
|
|
66
69
|
/** feed-forward artifact bound per step (prompt ingredient, spine-bound) */
|
|
67
70
|
const ARTIFACT_MAX = 2000;
|
|
71
|
+
/** how many changed paths ride the hitl pause's evidence package. Bounded like
|
|
72
|
+
* every other list this repo puts on a spine, and the trim ANNOUNCED (F28): the
|
|
73
|
+
* person needs to see WHAT moved, and a run that touched a thousand files is
|
|
74
|
+
* telling them something a thousand lines would not. */
|
|
75
|
+
const PAUSE_CHANGED_CAP = 50;
|
|
68
76
|
/** the wallet floor below which a replan is a stop, not an adaptation (review
|
|
69
77
|
* #5): a money-gate halt drains the wallet to ~0, so replanning against dust
|
|
70
78
|
* just burns another draft and mislabels the money-cut as "exits still red"
|
|
@@ -260,6 +268,60 @@ export function closeGapBlock(gap) {
|
|
|
260
268
|
+ (scrubbed.length > CHECK_GAP_MAX ? boundGap(scrubbed, REPLAN_GAP_KEEP) : scrubbed);
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
|
|
273
|
+
* provider string (`denied:<tool>` today, ~20 bytes), not a gap, so this is a
|
|
274
|
+
* backstop against a future terminal that arrives long — never an envelope the
|
|
275
|
+
* shipped path routinely rides through.
|
|
276
|
+
*/
|
|
277
|
+
export const BOUND_REASON_MAX = 200;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* What the NEXT attempt is told about the bound that cut the previous one.
|
|
281
|
+
*
|
|
282
|
+
* Two different bounds used to set the same bare `attemptBounded = roundIteration`
|
|
283
|
+
* and therefore render the same sentence. For a round-bound cutoff that sentence is
|
|
284
|
+
* true. For BA-11's deny streak it is false twice over — the rounds did NOT run out
|
|
285
|
+
* (the fence short-circuited the attempt with budget left), and the count it quotes
|
|
286
|
+
* is the cap, not what was spent — and it aims the worker at the READ BUDGET when the
|
|
287
|
+
* thing that stopped it was the FENCE. The reason was already recorded on the spine;
|
|
288
|
+
* nothing carried it to the one reader who could act on it.
|
|
289
|
+
*
|
|
290
|
+
* The denial note is MECHANICAL in the strict sense: it quotes the terminal the
|
|
291
|
+
* provider actually returned and names which bound fired. It invents nothing —
|
|
292
|
+
* no streak count and no denied path, because bare-agent's return (`error:
|
|
293
|
+
* 'denied:<tool>'`, loop.js) carries neither, and a number nobody measured is a
|
|
294
|
+
* bug with a confident voice. Every other cause keeps the frozen wording byte for
|
|
295
|
+
* byte: a wall-bounded attempt reads as a round bound exactly as it did before
|
|
296
|
+
* (and is unreachable anyway — the step head refuses to fund an attempt past the
|
|
297
|
+
* deadline, W-2), so this change can only ever speak where it has something true
|
|
298
|
+
* to add.
|
|
299
|
+
*
|
|
300
|
+
* The reason arrives ALREADY SCRUBBED (the capture seam in `mkWorker` runs it
|
|
301
|
+
* through the one `SECRET_PATTERNS` inventory), so this renderer only bounds it —
|
|
302
|
+
* and announces the trim with the repo's ONE marker rather than a second spelling
|
|
303
|
+
* of "this text was cut" (F90.2).
|
|
304
|
+
*
|
|
305
|
+
* @param {{iteration?: number|string|undefined, cause: string, reason: string|null}|null|undefined} bounded
|
|
306
|
+
* the previous attempt's bound state, or nullish when it was not bounded
|
|
307
|
+
* @param {number} rounds the step's round bound, as quoted by the frozen sentence
|
|
308
|
+
* @returns {string|null} the note, or null when there is nothing to say
|
|
309
|
+
*/
|
|
310
|
+
export function boundedNote(bounded, rounds) {
|
|
311
|
+
if (!bounded) return null;
|
|
312
|
+
if (bounded.cause !== 'denied') {
|
|
313
|
+
return `Your previous attempt was CUT OFF after ${rounds} tool rounds. `
|
|
314
|
+
+ 'Reading is bounded; writing is not. Form a hypothesis EARLY and make the change.';
|
|
315
|
+
}
|
|
316
|
+
const raw = String(bounded.reason ?? '');
|
|
317
|
+
const shown = raw.length > BOUND_REASON_MAX
|
|
318
|
+
? `${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}]`
|
|
319
|
+
: raw;
|
|
320
|
+
return 'Your previous attempt was CUT OFF by the gate: it denied consecutive tool calls '
|
|
321
|
+
+ `until the streak guard ended the attempt. The gate recorded: ${shown}. `
|
|
322
|
+
+ 'The round budget was not what stopped you — repeating a denied call ends this attempt the same way.';
|
|
323
|
+
}
|
|
324
|
+
|
|
263
325
|
/**
|
|
264
326
|
* The plan-drafting prompt: a schema DESCRIPTION built from the live validator
|
|
265
327
|
* menus — never a copyable example (the drafter must author, not echo; the
|
|
@@ -321,7 +383,7 @@ Shape: { "schema": "plan-v1", "steps": [ ... 1..${MAX_PLAN_STEPS} steps ... ] }
|
|
|
321
383
|
strictly in array order. Each step (no other fields exist):
|
|
322
384
|
- "id": kebab-case slug, unique
|
|
323
385
|
- "action": the step's task, precise enough for a worker that sees ONLY this step
|
|
324
|
-
- "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (write/edit change the tree; recall/get/impact/related/recent search and navigate the repository index; compress/peek read cheaply; stash/remember/forget park and record notes across steps)
|
|
386
|
+
- "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)
|
|
325
387
|
- "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
|
|
326
388
|
- "target": the step's deliverable path (REQUIRED when tools include write/edit), inside ${JSON.stringify(job.writeScope)}
|
|
327
389
|
- "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
|
|
@@ -351,6 +413,15 @@ strictly in array order. Each step (no other fields exist):
|
|
|
351
413
|
exit reds on that file can never finish, on any attempt.${seedRedLaw}${priorChecksLaw}
|
|
352
414
|
Reference checks by NAME only; you cannot author or modify one.
|
|
353
415
|
|
|
416
|
+
The worker has NO SHELL. It cannot run a command, a compiler, a linter, or a test suite,
|
|
417
|
+
and it cannot execute a script it writes: its only verbs are the tools you grant that step
|
|
418
|
+
in "tools". The check named in a "check-passes" exit is the ONLY execution in this run —
|
|
419
|
+
the shell runs it after each attempt and hands the worker its raw output. So write every
|
|
420
|
+
"action" for a worker that locates its work by READING and GREPPING files and by reading
|
|
421
|
+
the check output it is given. Never tell it to run, execute, re-run, or verify by running
|
|
422
|
+
anything (an action that opens with "Run \`<command>\`…" costs the whole step: the worker
|
|
423
|
+
has no way to obey it and spends its rounds hunting by hand).
|
|
424
|
+
|
|
354
425
|
Offered "scope" values for tree-changed — copy ONE of these exactly, character for
|
|
355
426
|
character. No other value is accepted, and patterns of your own (like "src/*.js")
|
|
356
427
|
are not accepted:
|
|
@@ -518,12 +589,24 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
518
589
|
* it the deterministic name would collide with that very branch and the collision walk
|
|
519
590
|
* would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
|
|
520
591
|
* path. A recorded branch that no longer exists is a STOP, never a fresh start.
|
|
592
|
+
* @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
593
|
+
* answer at a hitl pause (2026-08-12 §1, re-cut 2026-08-17: accept | rerun <text> | pause), carried in
|
|
594
|
+
* by the runner on the leg that RESUMES a paused run. Absent on every ordinary run and
|
|
595
|
+
* on the leg that pauses; it is never authored, never defaulted, and never inferred.
|
|
596
|
+
*
|
|
597
|
+
* It is spent ONCE, on this leg's close readings up to the moment the fix loop opens:
|
|
598
|
+
* `accept` greens the human stage on fresh evidence (OPEN-3 — the mechanical stages
|
|
599
|
+
* re-run first, because the tree can change while a run is paused), `rerun` reds it
|
|
600
|
+
* with the human's own words as the gap, and `pause` keeps the checkpoint exactly as it
|
|
601
|
+
* is, before anything is read at all. After the fix loop opens the ruling is gone, so once the mechanical
|
|
602
|
+
* stages pass again the run PAUSES for a second review rather than converting the same
|
|
603
|
+
* sentence forever.
|
|
521
604
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
522
605
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
523
606
|
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
|
|
524
|
-
* 'step-stalled' | `step-red:<id>`
|
|
607
|
+
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
525
608
|
*/
|
|
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 }) {
|
|
609
|
+
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, humanRuling = null }) {
|
|
527
610
|
workdir = resolve(workdir);
|
|
528
611
|
// ONE spelling of the redaction, housed next to the inventory it reads
|
|
529
612
|
// (src/validate.js) — the same helper the isolate verbs scrub the litectx store
|
|
@@ -575,6 +658,79 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
575
658
|
return 'close-unsupported';
|
|
576
659
|
}
|
|
577
660
|
|
|
661
|
+
// ── THE SIGNER'S ANSWER (N4 §1.4), read before anything costs anything.
|
|
662
|
+
//
|
|
663
|
+
// Three gates, and each refuses rather than guesses. A decision that is not one
|
|
664
|
+
// of the three doors — or a `rerun` whose text is empty — is refused at the
|
|
665
|
+
// seam that ACCEPTS it, because the whole of what a rerun gives the fix worker
|
|
666
|
+
// is the human's words, and an empty gap re-runs the worker as though nothing
|
|
667
|
+
// had been said (the POC's negative control measured exactly that). A decision
|
|
668
|
+
// handed to a close with no human stage is refused too: there is nothing for it
|
|
669
|
+
// to rule on, and silently ignoring it would leave a person believing they had
|
|
670
|
+
// answered. It is a terminal of its own rather than an `interpreter-red` — no
|
|
671
|
+
// library failed, the operator's own input is what could not be read.
|
|
672
|
+
const hasHumanStage = stagedClose.some((/** @type {any} */ s) => s?.kind === HUMAN_KIND);
|
|
673
|
+
const ruling = normalizeHumanRuling(humanRuling);
|
|
674
|
+
const decisionRed = (/** @type {string} */ detail) => {
|
|
675
|
+
emit('escalation', {
|
|
676
|
+
category: HITL_DECISION_RED, decisionReady: true,
|
|
677
|
+
decision: 'The decision handed to this run could not be applied, so nothing was run and nothing was spent.',
|
|
678
|
+
options: [
|
|
679
|
+
`re-run the resume with one of: ${HUMAN_DECISIONS.join(' | ')} (a rerun carries the text the worker converts)`,
|
|
680
|
+
'abandon the task',
|
|
681
|
+
],
|
|
682
|
+
detail: scrub(detail),
|
|
683
|
+
});
|
|
684
|
+
return HITL_DECISION_RED;
|
|
685
|
+
};
|
|
686
|
+
if (!ruling.ok) return decisionRed(/** @type {string} */ (ruling.why));
|
|
687
|
+
if (ruling.ruling !== null && !hasHumanStage) {
|
|
688
|
+
return decisionRed(`this job's close has no ${HUMAN_KIND} stage, so a signer's "${ruling.ruling.decision}" has `
|
|
689
|
+
+ 'nothing to rule on — the verdict class is a promise about the CLOSE, and only a close that asks a person '
|
|
690
|
+
+ 'can be answered by one');
|
|
691
|
+
}
|
|
692
|
+
/** the ruling still to be spent. Cleared the moment the fix loop opens: the
|
|
693
|
+
* human's words are what OPENS that loop, and once the worker holds them the
|
|
694
|
+
* next machine-clean tree is a new question for the person, not the old one. */
|
|
695
|
+
let liveRuling = ruling.ruling;
|
|
696
|
+
|
|
697
|
+
// ── PAUSE: the third door, and it costs nothing at all (2026-08-12 §1, doors
|
|
698
|
+
// re-cut 2026-08-17). Before the branch, the seed, the precheck and the clock,
|
|
699
|
+
// because a person who answered "not now" has asked for nothing to be done: no
|
|
700
|
+
// gap, no continuation, no worker round. It is deliberately NOT routed through
|
|
701
|
+
// the close — running four stages to arrive back at the question the person has
|
|
702
|
+
// just declined to answer is wall time spent on an answer already in hand.
|
|
703
|
+
//
|
|
704
|
+
// hamr's ruling, verbatim: *"what's the point of cancel anyways? pause can
|
|
705
|
+
// resume — that would be more honest"*. So this mints the SAME checkpoint
|
|
706
|
+
// terminal the machine-side pause mints, which is what keeps it resumable under
|
|
707
|
+
// the existing TTL: the run re-enters at the start of its last step whenever
|
|
708
|
+
// somebody comes back, and if nobody does, the checkpoint expires on its own.
|
|
709
|
+
// That expiry IS the case cancel used to serve, without forcing a person into a
|
|
710
|
+
// forever-decision at the moment they least want to make one.
|
|
711
|
+
//
|
|
712
|
+
// The record is EXPLICIT (`humanDecision`) rather than a silent re-pause: "a
|
|
713
|
+
// person looked and kept it" and "nobody has looked yet" are two different
|
|
714
|
+
// facts, and one record spelling both is how a reader comes to confuse them.
|
|
715
|
+
// The evidence package is not re-assembled here — it is already on the spine of
|
|
716
|
+
// the leg that asked, and this leg measured nothing, so it states nothing (F6:
|
|
717
|
+
// absent, never an empty list dressed as a reading).
|
|
718
|
+
if (liveRuling?.decision === 'pause') {
|
|
719
|
+
emit(HITL_PAUSE, {
|
|
720
|
+
stage: stagedClose.find((/** @type {any} */ s) => s?.kind === HUMAN_KIND)?.name ?? null,
|
|
721
|
+
humanDecision: 'pause',
|
|
722
|
+
decisionReady: true,
|
|
723
|
+
// EXPLICITLY null, never absent: every gap consumer guards with `if (gap)`,
|
|
724
|
+
// and the field states the ruling rather than leaving it to be inferred.
|
|
725
|
+
gap: null,
|
|
726
|
+
decision: 'The signer looked and paused: nothing was asked for, so nothing was run and nothing was spent. The '
|
|
727
|
+
+ 'checkpoint stands exactly as it was — the work, the plan and the money are where the paused leg left them.',
|
|
728
|
+
options: [...HUMAN_DECISIONS],
|
|
729
|
+
meaning: 'not a verdict — the run is still paused, the clock is still stopped, and the same three doors are open',
|
|
730
|
+
});
|
|
731
|
+
return HITL_PAUSE;
|
|
732
|
+
}
|
|
733
|
+
|
|
578
734
|
// ── native wiring: a clipipe-subscription job needs the native provider
|
|
579
735
|
// FACTORY (native governance is constructor-time + per-worker, so the runner
|
|
580
736
|
// cannot reuse one injected instance). A missing factory is an adopter wiring
|
|
@@ -764,7 +920,14 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
764
920
|
* @param {any[]} stages
|
|
765
921
|
*/
|
|
766
922
|
const runCloseStages = (stages) => (declared
|
|
767
|
-
? runDeclaredStages(stages, scrub, {
|
|
923
|
+
? runDeclaredStages(stages, scrub, {
|
|
924
|
+
...closeOpts,
|
|
925
|
+
seedRef: /** @type {any} */ (declaredCtx).seedRef,
|
|
926
|
+
// the signer's answer reaches the human stage the same way every other
|
|
927
|
+
// arbiter-owned fact reaches a stage: through the ctx, never through the
|
|
928
|
+
// declaration. A command close has no human stage to hand it to.
|
|
929
|
+
humanRuling: liveRuling,
|
|
930
|
+
})
|
|
768
931
|
: runStages(stages, scrub, closeOpts));
|
|
769
932
|
/** @type {string|undefined} the stage that rendered the LAST close verdict (Layer R's red-set source) */
|
|
770
933
|
let closeStage;
|
|
@@ -821,6 +984,41 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
821
984
|
...extra,
|
|
822
985
|
});
|
|
823
986
|
};
|
|
987
|
+
/**
|
|
988
|
+
* THE PAUSE (N4 §1.3/§1.4, rulings 1 and 2) — a decision-ready checkpoint, and
|
|
989
|
+
* never a bare "approve?". What the person is shown is assembled here rather
|
|
990
|
+
* than by whatever surface renders it: every mechanical stage's own result, the
|
|
991
|
+
* question the close itself declared, and the set of files this run changed.
|
|
992
|
+
*
|
|
993
|
+
* The changed set is the "before/after" half, at the granularity a library can
|
|
994
|
+
* state honestly — WHICH files moved. Rendering the lines belongs to the
|
|
995
|
+
* surface that has a screen; carrying the list is what makes the surface's job
|
|
996
|
+
* possible without it re-deriving the run's own facts.
|
|
997
|
+
* @param {any} v the close verdict that paused @returns {Promise<void>}
|
|
998
|
+
*/
|
|
999
|
+
const emitHitlPause = async (v) => {
|
|
1000
|
+
const cs = declaredCtx ? await changedSet(workdir, declaredCtx.seedRef) : { stop: 'no seed', paths: [] };
|
|
1001
|
+
const paths = cs.stop === null ? cs.paths : [];
|
|
1002
|
+
const shown = paths.slice(0, PAUSE_CHANGED_CAP);
|
|
1003
|
+
emit(HITL_PAUSE, {
|
|
1004
|
+
stage: v.stage ?? null,
|
|
1005
|
+
ask: v.ask ?? null,
|
|
1006
|
+
decisionReady: true,
|
|
1007
|
+
// ruling 2: the whole evidence package, never a bare "approve?"
|
|
1008
|
+
stages: v.stages ?? [],
|
|
1009
|
+
changed: {
|
|
1010
|
+
paths: shown,
|
|
1011
|
+
// the trim is ANNOUNCED (F28), and an unreadable set is UNKNOWN, never
|
|
1012
|
+
// an empty list dressed as "nothing changed" (F6)
|
|
1013
|
+
...(paths.length > shown.length ? { more: paths.length - shown.length } : {}),
|
|
1014
|
+
...(cs.stop === null ? {} : { unreadable: scrub(String(cs.stop)) }),
|
|
1015
|
+
},
|
|
1016
|
+
decision: `The close reached its human stage and is waiting on you: ${v.ask ?? 'review the result'}`,
|
|
1017
|
+
options: [...HUMAN_DECISIONS],
|
|
1018
|
+
meaning: 'not a verdict — the run is paused, the clock is stopped, and the last thing it did stands until you answer',
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
|
|
824
1022
|
/** the close, as ONE verdict: every stage in order, first red wins */
|
|
825
1023
|
const judgeClose = async () => {
|
|
826
1024
|
const v = await runCloseStages(stagedClose);
|
|
@@ -994,6 +1192,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
994
1192
|
const pre = await judgeClose();
|
|
995
1193
|
emit('close-precheck', { ...pre });
|
|
996
1194
|
if (pre.verdict === 'satisfied') return 'already-green';
|
|
1195
|
+
// …and the pause's own arm of the same gate. Reaching a human stage HERE means
|
|
1196
|
+
// every mechanical stage already passes on the untouched tree: the machine half
|
|
1197
|
+
// of this job is done and the only thing left is a person. Pausing costs $0 and
|
|
1198
|
+
// is the honest answer; running a plan first would spend a budget to arrive at
|
|
1199
|
+
// exactly this question. No `planExecuted` — there is no plan yet, and an empty
|
|
1200
|
+
// one on the spine would be a record of work nobody did.
|
|
1201
|
+
if (pre.verdict === HUMAN_PAUSE) { await emitHitlPause(pre); return HITL_PAUSE; }
|
|
997
1202
|
const preFault = Object.hasOwn(CLOSE_FAULTS, pre.verdict) ? CLOSE_FAULTS[pre.verdict] : undefined;
|
|
998
1203
|
if (preFault) {
|
|
999
1204
|
emit('escalation', { category: preFault.category, decisionReady: true, decision: preFault.decision, options: preFault.options, detail: pre.detail });
|
|
@@ -1062,7 +1267,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1062
1267
|
const lc = new LiteCtx({ root: workdir });
|
|
1063
1268
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
1064
1269
|
const fencePrefixes = job.writeScope.map((/** @type {string} */ g) => resolve(workdir, globToPrefix(g)));
|
|
1065
|
-
const auditPath = join(workdir,
|
|
1270
|
+
const auditPath = join(workdir, GATE_AUDIT_FILE);
|
|
1066
1271
|
const chainByName = new Map(menu.map((m) => [m.name, m.run]));
|
|
1067
1272
|
// Choose-don't-describe (§4): the offered `tree-changed` scopes, enumerated from
|
|
1068
1273
|
// the signed fence plus the directories that actually exist beneath it. ONE menu
|
|
@@ -1198,7 +1403,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1198
1403
|
// uses), so this can only tighten, never widen
|
|
1199
1404
|
writeScope: writable ? (fence ?? fencePrefixes) : [],
|
|
1200
1405
|
readScope: [workdir],
|
|
1201
|
-
deny: [auditPath,
|
|
1406
|
+
deny: [auditPath, ...ARBITER_BOOK_STORES.map((s) => join(workdir, s))],
|
|
1202
1407
|
},
|
|
1203
1408
|
budget: { maxCostUsd: Math.max(remainingUsd(), 0.0001) },
|
|
1204
1409
|
limits: { maxTurns },
|
|
@@ -1312,7 +1517,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1312
1517
|
/** @type {number|string|undefined} */
|
|
1313
1518
|
let roundIteration;
|
|
1314
1519
|
let roundsThisAttempt = 0;
|
|
1315
|
-
/**
|
|
1520
|
+
/**
|
|
1521
|
+
* WHICH bound cut the last attempt, and — when the provider recorded one — its
|
|
1522
|
+
* reason. A bare iteration number was the whole state here, so three different
|
|
1523
|
+
* bounds (round cap, wall, BA-11 deny streak) were indistinguishable to the one
|
|
1524
|
+
* consumer that speaks to the worker, which then told every one of them the
|
|
1525
|
+
* round cap had run out. The cause travels WITH the iteration because they are
|
|
1526
|
+
* read together and can never be allowed to drift apart.
|
|
1527
|
+
*
|
|
1528
|
+
* `reason` is scrubbed at THIS seam — the boundary, once, before any reader —
|
|
1529
|
+
* so no consumer has to remember to do it (the same placement the exit results
|
|
1530
|
+
* get in `judge`). It rides the spine event in the same scrubbed spelling: one
|
|
1531
|
+
* value, one inventory, and an append-only log is forever.
|
|
1532
|
+
* `iteration` mirrors `roundIteration` exactly, undefined included: the bound
|
|
1533
|
+
* cannot know a label the run never set, and inventing one here would be the
|
|
1534
|
+
* only place in this file that claims to.
|
|
1535
|
+
* @type {{iteration: number|string|undefined, cause: 'rounds'|'wall'|'denied', reason: string|null}|undefined}
|
|
1536
|
+
*/
|
|
1316
1537
|
let attemptBounded;
|
|
1317
1538
|
/**
|
|
1318
1539
|
* Open an iteration: label the rounds, reset the per-iteration round counter,
|
|
@@ -1413,19 +1634,24 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1413
1634
|
}
|
|
1414
1635
|
// a maxTurns session is a BOUNDED attempt, not an escalation — the same
|
|
1415
1636
|
// role loop.stop() plays on the Loop path: judge the partial work and
|
|
1416
|
-
// feed the gap forward (the CLI preserves lastText, BA-5).
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
emit('attempt-bounded', { phase, iteration: roundIteration, cap: attemptRounds, native: true });
|
|
1420
|
-
return r;
|
|
1421
|
-
}
|
|
1637
|
+
// feed the gap forward (the CLI preserves lastText, BA-5). A DENIAL
|
|
1638
|
+
// STREAK is the same shape and takes the same lane (see the Loop path's
|
|
1639
|
+
// note below — one doctrine, both surfaces).
|
|
1422
1640
|
if (r.error) {
|
|
1641
|
+
if (r.error === 'max_turns' || r.error.startsWith('denied:')) {
|
|
1642
|
+
// the CAUSE is kept, not just the fact: `max_turns` really is the round
|
|
1643
|
+
// cap, a `denied:` terminal is the fence, and the next attempt is told
|
|
1644
|
+
// which one it was (`boundedNote`)
|
|
1645
|
+
const denied = r.error.startsWith('denied:');
|
|
1646
|
+
const reason = scrub(r.error);
|
|
1647
|
+
attemptBounded = { iteration: roundIteration, cause: denied ? 'denied' : 'rounds', reason };
|
|
1648
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, cap: attemptRounds, native: true, reason });
|
|
1649
|
+
return r;
|
|
1650
|
+
}
|
|
1423
1651
|
const err = /** @type {CategorizedError} */ (new Error(`native session: ${r.error}`));
|
|
1424
|
-
// halt → cap-halt
|
|
1425
|
-
//
|
|
1426
|
-
err.category = r.error.startsWith('halt:') ? 'cap-halt'
|
|
1427
|
-
: r.error.startsWith('denied:') ? 'gate-red'
|
|
1428
|
-
: 'provider-red';
|
|
1652
|
+
// halt → cap-halt; a bridge/session terminal (bridge-failed,
|
|
1653
|
+
// session_timeout, session:*) is provider-owned transport
|
|
1654
|
+
err.category = r.error.startsWith('halt:') ? 'cap-halt' : 'provider-red';
|
|
1429
1655
|
err.lib = 'bare-agent';
|
|
1430
1656
|
throw err;
|
|
1431
1657
|
}
|
|
@@ -1523,11 +1749,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1523
1749
|
// (a step that would BEGIN past the deadline is never funded), and the head of
|
|
1524
1750
|
// a close-fix iteration (W-2 — the run stops on the verdict already minted).
|
|
1525
1751
|
if (clock.expired()) {
|
|
1526
|
-
attemptBounded = roundIteration;
|
|
1752
|
+
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
1527
1753
|
emit('wall-bounded', { phase, iteration: roundIteration, ...clock.report(closeTimeoutForReport) });
|
|
1528
1754
|
self.stop();
|
|
1529
1755
|
} else if (roundsThisAttempt >= attemptRounds) {
|
|
1530
|
-
attemptBounded = roundIteration;
|
|
1756
|
+
attemptBounded = { iteration: roundIteration, cause: 'rounds', reason: null };
|
|
1531
1757
|
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds });
|
|
1532
1758
|
self.stop();
|
|
1533
1759
|
}
|
|
@@ -1561,12 +1787,25 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1561
1787
|
} catch (e) {
|
|
1562
1788
|
throw categorize(e).err;
|
|
1563
1789
|
}
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1790
|
+
// BA-11's deny streak is a BOUNDED ATTEMPT, not a terminal. It is the same
|
|
1791
|
+
// shape `loop.stop()` has at the round bound thirty lines up: the attempt
|
|
1792
|
+
// ran, produced real work, and was cut short by a governance bound doing
|
|
1793
|
+
// exactly its job — the fence HELD, which is the opposite of a fault. So it
|
|
1794
|
+
// takes the same lane: judge the partial work, feed the gap forward, the
|
|
1795
|
+
// loop continues under unchanged caps. Routing it as a terminal killed a
|
|
1796
|
+
// converging run with money and wall left (u-msn227nq). The native surface
|
|
1797
|
+
// takes the identical lane above — one doctrine, both surfaces.
|
|
1798
|
+
if (r.error && r.error.startsWith('denied:')) {
|
|
1799
|
+
const reason = scrub(r.error);
|
|
1800
|
+
attemptBounded = { iteration: roundIteration, cause: 'denied', reason };
|
|
1801
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds, reason });
|
|
1802
|
+
return r;
|
|
1803
|
+
}
|
|
1804
|
+
// the remaining error-return taxonomy (one map, same doctrine as native's):
|
|
1805
|
+
// halt → cap-halt, API truncation → provider-red, anything else is wiring
|
|
1566
1806
|
if (r.error) {
|
|
1567
1807
|
const err = /** @type {CategorizedError} */ (new Error(`worker loop: ${r.error}`));
|
|
1568
1808
|
err.category = r.error.startsWith('halt:') ? 'cap-halt'
|
|
1569
|
-
: r.error.startsWith('denied:') ? 'gate-red'
|
|
1570
1809
|
: r.error.startsWith('truncated:') ? 'provider-red'
|
|
1571
1810
|
: 'interpreter-red';
|
|
1572
1811
|
err.lib = 'bare-agent';
|
|
@@ -2044,14 +2283,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2044
2283
|
// carries counts and paths only — never content (the spine is forever).
|
|
2045
2284
|
const rootInj = root ? root.observe({ iteration, gap, writes: w.workerWrites() }) : null;
|
|
2046
2285
|
if (rootInj) emit('root-injected', { step: step.id, ...rootInj.event });
|
|
2286
|
+
// read ONCE: the note and the "was it the attempt just before this one" test
|
|
2287
|
+
// are two questions about the same record, and two reads is how they drift
|
|
2288
|
+
const bound = w.wasBounded();
|
|
2047
2289
|
const r = await w.ask([
|
|
2048
2290
|
step.action,
|
|
2049
2291
|
`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.`,
|
|
2050
2292
|
step.target && `Write your deliverable to: ${resolve(workdir, step.target)}`,
|
|
2051
2293
|
artifacts.length > 0 && `Working context (read-only) — prior steps' results:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
|
|
2052
2294
|
gap && `Previous attempt failed this step's checks:\n${gap}`,
|
|
2053
|
-
|
|
2054
|
-
|
|
2295
|
+
// the note names the bound that ACTUALLY fired — a deny streak and a spent
|
|
2296
|
+
// round cap are different diagnoses with different remedies, and telling a
|
|
2297
|
+
// fenced worker to "form a hypothesis early" aims it at the wrong wall
|
|
2298
|
+
bound?.iteration === iteration - 1 && boundedNote(bound, step.rounds),
|
|
2055
2299
|
rootInj && rootInj.note,
|
|
2056
2300
|
].filter(Boolean).join('\n\n'));
|
|
2057
2301
|
lastText = scrub(r.text ?? '').slice(0, ARTIFACT_MAX);
|
|
@@ -2234,7 +2478,32 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2234
2478
|
&& cat === 'step-variance'
|
|
2235
2479
|
&& !varianceGrantUsed
|
|
2236
2480
|
&& stopTrend.trend === 'converging';
|
|
2237
|
-
|
|
2481
|
+
// W-2 at the replan GATE, hamr's ruling: *"past the wall no new fix cycle or step
|
|
2482
|
+
// starts"* — and a replan is the first move of starting a new step. Past the
|
|
2483
|
+
// deadline the whole cycle is doomed by construction and it is not free: the
|
|
2484
|
+
// drafting call is bought (money the operator's time allowance no longer covers),
|
|
2485
|
+
// the redrafted plan's first step is then refused at its own head by the W-2 check
|
|
2486
|
+
// one level down, and the run wall-halts anyway. u-msmt91t3 paid for exactly that
|
|
2487
|
+
// and called the result `step-red`. So the gate declines to FUND it.
|
|
2488
|
+
//
|
|
2489
|
+
// Nothing is routed here and no readout is duplicated: declining simply lets the
|
|
2490
|
+
// stop fall through to the `step-variance` terminal below, which is the one site
|
|
2491
|
+
// that mints the wall-halt package (record + F11-consistent escalation + levers).
|
|
2492
|
+
// One site, one spelling — a second copy here is how two instruments come to
|
|
2493
|
+
// disagree about one event.
|
|
2494
|
+
//
|
|
2495
|
+
// TRIGGER-AGNOSTIC (hamr: "extend"). It first covered the variance trigger only,
|
|
2496
|
+
// the one measured on u-msmt91t3; the ladder's exhaustion and the stall fuse reach
|
|
2497
|
+
// this gate past the deadline too, and a doomed cycle costs the same money whichever
|
|
2498
|
+
// instrument named the stop. Each trigger then lands on its own honest terminal
|
|
2499
|
+
// below — the wall for the two that would otherwise mint `step-red`, its own name
|
|
2500
|
+
// for the stall (which already rides out as `step-stalled` and must, because run.js
|
|
2501
|
+
// keys the F44 spend FLOOR on that outcome).
|
|
2502
|
+
//
|
|
2503
|
+
// The latch is NOT consumed — no grant was spent, because no replan was bought.
|
|
2504
|
+
// With time on the clock this is byte-identical to today, for every trigger.
|
|
2505
|
+
const wallStopsReplan = clock.expired();
|
|
2506
|
+
if (!wallStopsReplan && (!replanned || grantExtraReplan) && replanTrigger && remainingUsd() > MONEY_MIN) {
|
|
2238
2507
|
if (grantExtraReplan) varianceGrantUsed = true;
|
|
2239
2508
|
replanned = true;
|
|
2240
2509
|
replans += 1;
|
|
@@ -2333,6 +2602,38 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2333
2602
|
skipCount = 0;
|
|
2334
2603
|
continue;
|
|
2335
2604
|
}
|
|
2605
|
+
/**
|
|
2606
|
+
* W-2's STEP-LOOP terminal, and the ONE place it is spelled. Reached whenever a
|
|
2607
|
+
* trigger that would otherwise mint `step-red:<id>` lands with the deadline already
|
|
2608
|
+
* passed: the meter's stop (F85's variance) and the ladder's (attempt exhaustion
|
|
2609
|
+
* with money still on the table). Both are the same event wearing two instruments'
|
|
2610
|
+
* names — the clock is what ended the run — and `step-red` is a CAPABILITY label,
|
|
2611
|
+
* so minting it there files a governance stop as "the work failed". u-msmt91t3 is
|
|
2612
|
+
* the measured instance: `step-red` 9.9 seconds after the run's own `wall-bounded`.
|
|
2613
|
+
*
|
|
2614
|
+
* One site, one spelling, because the record, the F11-consistent escalation and the
|
|
2615
|
+
* lever list must be identical wherever the clock stops a step — a second copy per
|
|
2616
|
+
* trigger is how two instruments come to disagree about one event. The instrument
|
|
2617
|
+
* that named the stop FIRST is not discarded: it rides in the detail, so the human
|
|
2618
|
+
* reads why the step was stopped as well as which lever fits.
|
|
2619
|
+
*
|
|
2620
|
+
* `stepWallStop` is never consulted here by construction: that record is written
|
|
2621
|
+
* only by the head-of-attempt check, and both triggers above are read BEFORE it, so
|
|
2622
|
+
* a step either of them stopped never reached it.
|
|
2623
|
+
* @param {string} stoppedBy the instrument that named the stop first
|
|
2624
|
+
*/
|
|
2625
|
+
const wallHaltTerminal = (stoppedBy) => {
|
|
2626
|
+
emitWallHalt({ cutMidCall: false, phase: `step:${step.id}`, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
2627
|
+
emit('escalation', {
|
|
2628
|
+
category: 'wall-halt', decisionReady: true, phase: `step:${step.id}`,
|
|
2629
|
+
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.`,
|
|
2630
|
+
options: WALL_OPTIONS,
|
|
2631
|
+
detail: `requested ${clock.requestedMs}ms, elapsed ${clock.elapsedMs()}ms. `
|
|
2632
|
+
+ `${stoppedBy}: ${lastEscalation?.detail ?? '(none)'} `
|
|
2633
|
+
+ `Progress trend: ${stopTrend.trend} — ${stopTrend.reading}; ${stopTrend.lever}.`,
|
|
2634
|
+
});
|
|
2635
|
+
return 'wall-halt';
|
|
2636
|
+
};
|
|
2336
2637
|
// F64 — the wall stopped this attempt from INSIDE a provider call (the derived
|
|
2337
2638
|
// call timeout is the deadline in the provider's own coin). ralph already
|
|
2338
2639
|
// emitted the escalation under this category, so the outcome and the record
|
|
@@ -2355,7 +2656,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2355
2656
|
// stays a step-red (the stop is a result).
|
|
2356
2657
|
if (cat === 'cap-halt') {
|
|
2357
2658
|
planExecuted();
|
|
2358
|
-
if (remainingUsd() > MONEY_MIN)
|
|
2659
|
+
if (remainingUsd() > MONEY_MIN) {
|
|
2660
|
+
// W-2 EXTENDED to the ladder's stop (hamr: "extend"). This arm is the sibling of
|
|
2661
|
+
// the variance terminal below and carries the identical defect: with money still
|
|
2662
|
+
// on the table it minted `step-red` whatever the clock said, so a step the
|
|
2663
|
+
// deadline ended was filed as a capability failure. The wallet is re-read (it
|
|
2664
|
+
// always was); the clock is re-read too, and it is read FIRST inside this arm
|
|
2665
|
+
// only — a DRAINED wallet keeps its own `cap-halt` terminal below, because money
|
|
2666
|
+
// is the allowance actually blocking that run and its levers are the ones the
|
|
2667
|
+
// human needs (a wall-halt readout there would offer "raise maxWallMs and rerun"
|
|
2668
|
+
// into an immediate money cut).
|
|
2669
|
+
if (clock.expired()) return wallHaltTerminal('The ladder ended the step first');
|
|
2670
|
+
return `step-red:${step.id}`;
|
|
2671
|
+
}
|
|
2359
2672
|
// v1.46 §2: the money cut is decision-ready HERE too, and for the same reason
|
|
2360
2673
|
// W-2 gave the wall a record at every site it stops a run — a category that
|
|
2361
2674
|
// hands the human a readout at one seam and silence at another is the readout
|
|
@@ -2371,8 +2684,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2371
2684
|
// upward would mint an outcome run.js and the ledger's class table do not know,
|
|
2372
2685
|
// and an unmapped category is counted as a library bug (ledger.js) when this is a
|
|
2373
2686
|
// planning story. The escalation ralph already emitted carries the real detail.
|
|
2687
|
+
//
|
|
2688
|
+
// W-2 at the meter's own terminal. A `step-variance` stop that lands PAST the
|
|
2689
|
+
// deadline is a TIME stop wearing the meter's name — the clock is what ended the
|
|
2690
|
+
// run, and hamr's ruling names that stop: *"when time is up, keep the grade we
|
|
2691
|
+
// already have and stop"*. Its cap-halt sibling directly above already re-reads its
|
|
2692
|
+
// own governor (the wallet) before minting a terminal; this branch did not, and on
|
|
2693
|
+
// u-msmt91t3 it minted `step-red` 9.9 seconds AFTER the run's own `wall-bounded`
|
|
2694
|
+
// record — a governance stop filed as a capability read, and (the resume gate's
|
|
2695
|
+
// half of the same defect) a checkpoint filed as an answer.
|
|
2696
|
+
//
|
|
2697
|
+
// The clock is read HERE, with nothing in flight, exactly as the three other
|
|
2698
|
+
// terminal sites read it. With time still on the clock this is byte-identical to
|
|
2699
|
+
// what it was: the step-red is the designed stop, and the stop is the result.
|
|
2374
2700
|
if (cat === 'step-variance') {
|
|
2375
2701
|
planExecuted();
|
|
2702
|
+
if (clock.expired()) return wallHaltTerminal('The meter stopped the step first');
|
|
2376
2703
|
return `step-red:${step.id}`;
|
|
2377
2704
|
}
|
|
2378
2705
|
// Any OTHER terminal escalation category is NOT a capability failure: a
|
|
@@ -2394,6 +2721,16 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2394
2721
|
planExecuted();
|
|
2395
2722
|
return 'green';
|
|
2396
2723
|
}
|
|
2724
|
+
// ── THE THIRD BRANCH (N4 §1.3): between `satisfied` and the fix loop, because
|
|
2725
|
+
// a pause is neither. It is not green (nobody said so), not a red (nobody has
|
|
2726
|
+
// written a gap), and not a CLOSE_FAULT (nothing broke) — so it must be caught
|
|
2727
|
+
// before the fix loop, which would otherwise buy a worker round to convert a
|
|
2728
|
+
// wall that does not exist yet.
|
|
2729
|
+
if (post.verdict === HUMAN_PAUSE) {
|
|
2730
|
+
await emitHitlPause(post);
|
|
2731
|
+
planExecuted();
|
|
2732
|
+
return HITL_PAUSE;
|
|
2733
|
+
}
|
|
2397
2734
|
const postFault = Object.hasOwn(CLOSE_FAULTS, post.verdict) ? CLOSE_FAULTS[post.verdict] : undefined;
|
|
2398
2735
|
if (postFault) {
|
|
2399
2736
|
emit('escalation', { category: postFault.category, decisionReady: true, decision: postFault.decision, options: postFault.options, detail: post.detail });
|
|
@@ -2401,6 +2738,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2401
2738
|
return 'close-red';
|
|
2402
2739
|
}
|
|
2403
2740
|
emit('fix-loop', { gapBytes: Buffer.byteLength(post.gap ?? '') });
|
|
2741
|
+
// ── THE RULING IS SPENT HERE (N4 §1.5). The human's words are what OPENS this
|
|
2742
|
+
// loop; the worker now holds them. Leaving the ruling live would make the human
|
|
2743
|
+
// stage red on every judgment for the rest of the leg, so the loop would keep
|
|
2744
|
+
// converting one sentence until the governor or the money stopped it. Cleared,
|
|
2745
|
+
// the next machine-clean tree pauses for a SECOND review — which is the honest
|
|
2746
|
+
// cycle: a person asked for a change, the change happened, the person looks
|
|
2747
|
+
// again. A mechanical red in between is still converted normally, because
|
|
2748
|
+
// first-red-wins reaches it before the person's stage.
|
|
2749
|
+
liveRuling = null;
|
|
2404
2750
|
let fixOutcome;
|
|
2405
2751
|
/** @type {object|null} W-2 — set when the WALL stopped the fix loop BETWEEN
|
|
2406
2752
|
* iterations. It carries that stop's own record fields, and its presence is what
|
|
@@ -2482,6 +2828,15 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2482
2828
|
const fixGovernor = {
|
|
2483
2829
|
record: (/** @type {{iteration: number, gap?: string}} */ o) => {
|
|
2484
2830
|
fixIterationsUsed = o.iteration;
|
|
2831
|
+
// A PAUSE IS NEVER GRADED. ralph records every non-satisfied iteration,
|
|
2832
|
+
// and a pause is one — but it carries no gap and no number, so feeding it
|
|
2833
|
+
// to the trend would mint a strike out of a non-verdict and could strike
|
|
2834
|
+
// the loop out into a `cap-halt` that never happened. The reading says
|
|
2835
|
+
// what it is and the meter is left untouched; `middle` stops the loop on
|
|
2836
|
+
// the next iteration, before anything is bought.
|
|
2837
|
+
if (lastCloseVerdict?.verdict === HUMAN_PAUSE) {
|
|
2838
|
+
return { governor: 'close-trend', trend: 'unknown', reading: 'the close is waiting on a person — not a grade', iteration: o.iteration, paused: true };
|
|
2839
|
+
}
|
|
2485
2840
|
return { governor: 'close-trend', ...fixTrend.record({ gap: o.gap }), iteration: o.iteration };
|
|
2486
2841
|
},
|
|
2487
2842
|
struckOut: fixTrend.struckOut,
|
|
@@ -2523,6 +2878,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2523
2878
|
// until the next iteration is about to start. The close is never bounded — a
|
|
2524
2879
|
// wall that kills grading leaves the run unreadable after the money is spent
|
|
2525
2880
|
// (the F45 class), so the one thing this must never do is stop a close.
|
|
2881
|
+
// ── THE CLOSE IS WAITING ON A PERSON. Read here, in the same place and for
|
|
2882
|
+
// the same reason the wall is: past this point the iteration is guaranteed
|
|
2883
|
+
// to be worthless, because the machine half of the close already passes and
|
|
2884
|
+
// nothing a worker writes can answer a question only a person can. So the
|
|
2885
|
+
// loop stops ON that reading — the run pauses again, with everything it has
|
|
2886
|
+
// done kept, exactly as the first pause did.
|
|
2887
|
+
if (lastCloseVerdict?.verdict === HUMAN_PAUSE) {
|
|
2888
|
+
const err = /** @type {CategorizedError} */ (new Error(
|
|
2889
|
+
`the close is waiting on the signer at stage "${lastCloseVerdict.stage ?? 'unknown'}" — every mechanical `
|
|
2890
|
+
+ `stage passes, so fix attempt ${iteration} could not change the answer`));
|
|
2891
|
+
err.category = HITL_PAUSE;
|
|
2892
|
+
throw err;
|
|
2893
|
+
}
|
|
2526
2894
|
if (clock.expired()) {
|
|
2527
2895
|
// ONE progress instrument, shared with the money halt (PRD v1.46 §2/§4).
|
|
2528
2896
|
// This site used to run its own `gapTrend`: byte equality of the last two
|
|
@@ -2604,6 +2972,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2604
2972
|
return relay(e, 'fix');
|
|
2605
2973
|
}
|
|
2606
2974
|
planExecuted();
|
|
2975
|
+
// The fix loop ran into the person again (the `middle` stop above). Same
|
|
2976
|
+
// terminal as the first pause, and never `escalated`: nothing failed, and the
|
|
2977
|
+
// work this loop did is exactly what the signer is now being asked about.
|
|
2978
|
+
if (fixOutcome !== 'green' && lastEscalation?.category === HITL_PAUSE) {
|
|
2979
|
+
await emitHitlPause(lastCloseVerdict);
|
|
2980
|
+
return HITL_PAUSE;
|
|
2981
|
+
}
|
|
2607
2982
|
if (fixOutcome !== 'green' && lastEscalation?.category === 'wall-halt') {
|
|
2608
2983
|
// F64 in the close-fix loop: the same governance stop, and it must not ride out
|
|
2609
2984
|
// as a bare `escalated` either — that reads as "the fix failed" when the fix was
|