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/ralph.js
CHANGED
|
@@ -590,6 +590,11 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
|
|
|
590
590
|
const DECISIONS = {
|
|
591
591
|
'cap-halt': [`Budget gate tripped mid-run (${e && e.message}). Continue with a higher cap, change approach, or stop?`,
|
|
592
592
|
['raise the cap and rerun', 'change the middle/harness', 'abandon the task']],
|
|
593
|
+
// No site in this tree mints `gate-red` any more: a BA-11 denial streak is
|
|
594
|
+
// a bounded attempt now (planrun's two `ask`s), not a terminal, because the
|
|
595
|
+
// fence HOLDING is governance working, not a fault. The entry stays because
|
|
596
|
+
// this catch is a dumb passthrough — it files whatever category the thrower
|
|
597
|
+
// names, so an unlisted one would read out with the generic text.
|
|
593
598
|
'gate-red': ['The gate denied an action mid-run — the harness asked for something outside its scope.',
|
|
594
599
|
['widen the write scope deliberately', 'change the middle/harness', 'abandon the task']],
|
|
595
600
|
'provider-red': ['The provider path failed mid-run (transport, not logic) — no verdict exists and the spend for the failed call is unknown (F6).',
|
|
@@ -624,6 +629,15 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
|
|
|
624
629
|
// interpreter" would aim a human at the one component that did nothing wrong.
|
|
625
630
|
'step-stalled': ['The model stopped producing rounds and reissuing the call did not recover it — a stall, not a fault.',
|
|
626
631
|
['let the planner re-allocate what is left (replan)', 'retry the run', 'check provider status', 'abandon the task']],
|
|
632
|
+
// N4 — the close is WAITING ON A PERSON. Nothing failed and nothing is
|
|
633
|
+
// discarded: every mechanical stage passes and the one stage left is a
|
|
634
|
+
// question no worker can answer, so the loop stopped rather than buying
|
|
635
|
+
// an attempt that could not change the answer. The caller renders the
|
|
636
|
+
// real decision-ready package (`hitl-pause`, with the evidence); this
|
|
637
|
+
// entry exists so the passthrough never narrates a paused run as a
|
|
638
|
+
// broken middle.
|
|
639
|
+
'hitl-pause': ['The close reached its human stage — the run is paused, decision-ready, and the clock is stopped.',
|
|
640
|
+
['accept', 'rerun (your words become the gap the worker converts)', 'pause (the checkpoint keeps, and it resumes)']],
|
|
627
641
|
};
|
|
628
642
|
// Object.hasOwn, not bare lookup: a category named after an
|
|
629
643
|
// Object.prototype member ("toString") would return the inherited
|
package/src/reuse.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — the D7 ENVELOPE and the REUSE RUNNER (design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/02-features/2026-08-01-layer-3-reuse-design.md, D3/D5/D6/D7 + R1).
|
|
3
3
|
//
|
|
4
4
|
// hamr's sentence is the whole specification of this file, verbatim:
|
|
5
5
|
//
|
|
@@ -52,6 +52,12 @@ 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';
|
|
55
|
+
|
|
56
|
+
/** the paused-run TERMINAL and its spine record share one name (`runPlan` returns
|
|
57
|
+
* it and `runJob` writes it onto `job-end`), spelled once in `declaredclose.js`
|
|
58
|
+
* beside the close-verdict word it translates, so the emitters, the checkpoint
|
|
59
|
+
* list, and the TTL gate below cannot drift from each other */
|
|
60
|
+
import { HITL_PAUSE, HUMAN_CHECKPOINTS } from './declaredclose.js';
|
|
55
61
|
import { extractArtifact, priceOf } from './text.js';
|
|
56
62
|
import { runJob as shippedRunJob } from './run.js';
|
|
57
63
|
|
|
@@ -84,6 +90,34 @@ const { Loop } = require('bare-agent');
|
|
|
84
90
|
*/
|
|
85
91
|
export const REUSE_GRADED_RED = Object.freeze(['escalated']);
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* WHICH TERMINALS ARE A CHECKPOINT — the list a runner hands `readResume` as
|
|
95
|
+
* `resumableOutcomes`, minted HERE rather than in whichever script happens to
|
|
96
|
+
* launch a run.
|
|
97
|
+
*
|
|
98
|
+
* `readResume` deliberately takes it as a parameter and defaults to empty, and
|
|
99
|
+
* that stays true: the reuse loop's own semantics depend on the empty default.
|
|
100
|
+
* What did not exist was a canonical ANSWER, so `scripts/run-u.mjs` kept its own
|
|
101
|
+
* copy — and the exported bundle (PRD v1.44 §2: a thin runner with bareloop as a
|
|
102
|
+
* dependency) would have had to keep a third. The same reasoning that put the
|
|
103
|
+
* pause TTL in the library (OPEN-2, hamr, 2026-08-13) puts this here.
|
|
104
|
+
*
|
|
105
|
+
* Every entry is a stop that left WORK ON DISK and an allowance unspent — money,
|
|
106
|
+
* time, a self-healed stall, and now a person who has not answered yet. A verdict
|
|
107
|
+
* already rendered is never on this list: `green`/`escalated` are graded rows, and
|
|
108
|
+
* re-buying one would pay twice for an answer already in hand. (The signer's own
|
|
109
|
+
* `hitl-cancel` terminal used to be the other example; the door was deleted on
|
|
110
|
+
* 2026-08-17 — a person who does not want to carry on pauses, and the checkpoint
|
|
111
|
+
* they keep is retired by the TTL rather than by a terminal.)
|
|
112
|
+
*
|
|
113
|
+
* The human half is SPLICED IN from `declaredclose.js` rather than spelled again,
|
|
114
|
+
* because the try loop below reads the two halves differently and a second copy is
|
|
115
|
+
* how they would come to disagree about which member is which: the first three are
|
|
116
|
+
* allowances the loop's remaining tries are meant to spend against, and the human
|
|
117
|
+
* ones are the stop no further try can change (see `hardStop`).
|
|
118
|
+
*/
|
|
119
|
+
export const CHECKPOINT_OUTCOMES = Object.freeze(['cap-halt', 'wall-halt', 'step-stalled', ...HUMAN_CHECKPOINTS]);
|
|
120
|
+
|
|
87
121
|
/**
|
|
88
122
|
* v1's definition of "the same KIND of recipe" — and it is the LOAD GATE's own: the
|
|
89
123
|
* close-stage names, in the same order (`bridges.loadGate` rule 2). One predicate,
|
|
@@ -418,13 +452,24 @@ function readTry(events) {
|
|
|
418
452
|
};
|
|
419
453
|
}
|
|
420
454
|
|
|
421
|
-
/** the
|
|
422
|
-
*
|
|
423
|
-
* try loop and the resume reader classify through this — two
|
|
424
|
-
* of answer was that" is how a casualty ends up demoting a
|
|
425
|
-
*
|
|
455
|
+
/** the verdict read of a runJob outcome, in ONE place: a green, the one GRADED red
|
|
456
|
+
* that demotes (D6), a CHECKPOINT waiting on a person, or a casualty that keeps its
|
|
457
|
+
* own name. Both the live try loop and the resume reader classify through this — two
|
|
458
|
+
* spellings of "what kind of answer was that" is how a casualty ends up demoting a
|
|
459
|
+
* recipe.
|
|
460
|
+
*
|
|
461
|
+
* `checkpoint` is its own name rather than a fourth flavour of casualty (2026-08-17).
|
|
462
|
+
* A casualty is a run that DIED — a transport fault, a governance cut, a broken close
|
|
463
|
+
* — and a reader that sees one is being told the attempt is over. A human checkpoint
|
|
464
|
+
* is the opposite fact in every respect: nothing failed, nothing is discarded, the
|
|
465
|
+
* work and the money are exactly where they were, and the allowance still unspent is
|
|
466
|
+
* a person's answer. It behaves like neither a red nor a casualty everywhere it
|
|
467
|
+
* matters already (`hardStop` ends the loop on it, the box is left untouched), so the
|
|
468
|
+
* ROW saying "casualty" was the one place the machinery still mis-described itself.
|
|
469
|
+
* @param {string} outcome @returns {'green'|'red'|'checkpoint'|'casualty'} */
|
|
426
470
|
function verdictClassOf(outcome) {
|
|
427
471
|
if (outcome === 'green' || outcome === 'already-green') return 'green';
|
|
472
|
+
if (HUMAN_CHECKPOINTS.includes(outcome)) return 'checkpoint';
|
|
428
473
|
return REUSE_GRADED_RED.includes(outcome) ? 'red' : 'casualty';
|
|
429
474
|
}
|
|
430
475
|
|
|
@@ -665,7 +710,7 @@ function readGradeSeed(seen) {
|
|
|
665
710
|
* which is later and better evidence than the last event). Defaults to the last
|
|
666
711
|
* event's own timestamp — the last sign of life there is.
|
|
667
712
|
* @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
|
|
668
|
-
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
|
|
713
|
+
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, wallDerivedHalt, greened, completed, tried,
|
|
669
714
|
* selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
|
|
670
715
|
* spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
|
|
671
716
|
*/
|
|
@@ -816,7 +861,50 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
816
861
|
* CHECKPOINT and re-entering it is the ruling, not a second payment for an answer
|
|
817
862
|
* already in hand. Empty by default, which is what keeps the reuse loop's own
|
|
818
863
|
* graded-row semantics exactly where they were. */
|
|
819
|
-
const
|
|
864
|
+
const recordedOutcome = open?.jobEnd ? String(open.jobEnd.outcome) : null;
|
|
865
|
+
/** D4a — the MISLABELLED wall stop, derived from the run's own primary records.
|
|
866
|
+
*
|
|
867
|
+
* `step-red` is a verdict-shaped terminal and stays non-resumable: a red is an
|
|
868
|
+
* answer. But on u-msmt91t3 the runner minted one 9.9 seconds AFTER its own
|
|
869
|
+
* `wall-bounded` record — a governance stop wearing a capability label, because the
|
|
870
|
+
* step-variance terminal did not re-read the clock (fixed at the source, src/planrun.js).
|
|
871
|
+
* Spines already written under the defect are not re-runnable and cannot be edited (a
|
|
872
|
+
* spine is append-only forever), so the READER derives the truth the file already
|
|
873
|
+
* holds: a run whose own record says the wall was crossed BEFORE it ended is a wall
|
|
874
|
+
* stop, and W-2's ruling applies — it keeps the grades it minted and pauses.
|
|
875
|
+
*
|
|
876
|
+
* Three constraints, each of them load-bearing:
|
|
877
|
+
* * a DERIVATION, never an override. The evidence is the run's own `wall-bounded`
|
|
878
|
+
* event, emitted by the runner at the round seam; no caller can assert this.
|
|
879
|
+
* * `step-red` ONLY, and only when `wall-halt` is itself resumable for this caller.
|
|
880
|
+
* Every other outcome — green, escalated, provider-red, a step-red with no wall
|
|
881
|
+
* record — is untouched, so "a red is an answer" survives intact.
|
|
882
|
+
* * it touches NO close verdict. `step-red` here is a GOVERNANCE stop label; the
|
|
883
|
+
* grades the close rendered are exactly what they were, and the resume seeds
|
|
884
|
+
* from them.
|
|
885
|
+
* @param {any[]} seen @param {any} jobEnd */
|
|
886
|
+
const wallCrossedBefore = (seen, jobEnd) => {
|
|
887
|
+
// strictly BEFORE the terminal: a record written after the run ended could not have
|
|
888
|
+
// been what stopped it. `indexOf` on the window's own array is the ordering there is
|
|
889
|
+
// (the reader never re-sorts), and a jobEnd somehow absent from it reads nothing.
|
|
890
|
+
const end = seen.indexOf(jobEnd);
|
|
891
|
+
if (end < 0) return false;
|
|
892
|
+
for (let i = 0; i < end; i += 1) {
|
|
893
|
+
const e = seen[i];
|
|
894
|
+
if (!isObj(e) || e.type !== 'wall-bounded' || e.bounded !== true) continue;
|
|
895
|
+
// the clock's OWN two spellings of "the deadline passed", both off `clock.report`.
|
|
896
|
+
// Never a re-derivation from timestamps: a second arithmetic for one question is
|
|
897
|
+
// how two instruments come to disagree.
|
|
898
|
+
if (e.remainingMs === 0) return true;
|
|
899
|
+
if (typeof e.elapsedMs === 'number' && typeof e.requestedMs === 'number' && e.elapsedMs >= e.requestedMs) return true;
|
|
900
|
+
}
|
|
901
|
+
return false;
|
|
902
|
+
};
|
|
903
|
+
const wallDerivedHalt = recordedOutcome === 'step-red'
|
|
904
|
+
&& resumableHalt.has('wall-halt')
|
|
905
|
+
&& wallCrossedBefore(open.seen, open.jobEnd);
|
|
906
|
+
const haltedResumable = recordedOutcome !== null
|
|
907
|
+
&& (resumableHalt.has(recordedOutcome) || wallDerivedHalt);
|
|
820
908
|
if (open) {
|
|
821
909
|
if (open.jobEnd && !haltedResumable) {
|
|
822
910
|
// GRADED: runJob returned and the close had its say. Not a restart — the row is
|
|
@@ -963,7 +1051,14 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
963
1051
|
perTryBudgetUsd: head?.perTryBudgetUsd ?? directHead?.budgetUsd ?? null,
|
|
964
1052
|
perTryWallMs: head?.perTryWallMs ?? null,
|
|
965
1053
|
ended: end !== null || (directEnd !== null && !haltedResumable),
|
|
1054
|
+
// the RECORDED terminal, always — the derivation below never rewrites what the run
|
|
1055
|
+
// said about itself, it only says how this reader read it
|
|
966
1056
|
endOutcome: end?.outcome ?? directEnd?.outcome ?? null,
|
|
1057
|
+
/** D4a — true when a recorded `step-red` was re-read as a wall-halt off this run's
|
|
1058
|
+
* OWN `wall-bounded` record. Surfaced rather than silent: an operator resuming a
|
|
1059
|
+
* run whose spine says `step-red` must be told which record turned it into a
|
|
1060
|
+
* checkpoint, or the gate looks like it simply stopped refusing. */
|
|
1061
|
+
wallDerivedHalt,
|
|
967
1062
|
greened: completed.some((t) => t.verdictClass === 'green'),
|
|
968
1063
|
completed,
|
|
969
1064
|
tried,
|
|
@@ -973,6 +1068,81 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
973
1068
|
};
|
|
974
1069
|
}
|
|
975
1070
|
|
|
1071
|
+
/**
|
|
1072
|
+
* THE PAUSE TTL (2026-08-12 §2): a hitl checkpoint is kept for 60 days.
|
|
1073
|
+
*
|
|
1074
|
+
* OPEN-2, RULED (hamr, in-turn, 2026-08-13): **the TTL lives in the LIBRARY**, so
|
|
1075
|
+
* the exported bundle (a headless runner with bareloop as a dependency, PRD v1.44
|
|
1076
|
+
* §2) inherits it instead of every runner re-implementing it. A script-only TTL
|
|
1077
|
+
* would vanish on export — the one place the rule is most needed and least
|
|
1078
|
+
* likely to be re-typed.
|
|
1079
|
+
*/
|
|
1080
|
+
export const PAUSE_TTL_MS = 60 * 24 * 60 * 60_000;
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The AGE gate for a paused checkpoint — the sibling of `resumeTreeGate`, and it
|
|
1084
|
+
* asks the other question a 60-day-old checkpoint raises. The tree gate asks
|
|
1085
|
+
* *is this still the tree the run left?*; this asks *is this still a decision
|
|
1086
|
+
* anyone is waiting on?*
|
|
1087
|
+
*
|
|
1088
|
+
* THREE things it deliberately does NOT do:
|
|
1089
|
+
*
|
|
1090
|
+
* - it does not widen any other resume rule. The 2026-08-12 ruling is about a
|
|
1091
|
+
* person deciding, so `applies: false` on every other terminal: a cap-halt
|
|
1092
|
+
* checkpoint is a different stop under a different allowance, and quietly
|
|
1093
|
+
* ageing it out would be a governance change nobody ruled.
|
|
1094
|
+
* - it does not treat an unreadable stamp as young (F6). A checkpoint that
|
|
1095
|
+
* cannot say WHEN it paused has an unknown age, and unknown is refused rather
|
|
1096
|
+
* than rounded down to fresh — the direction that fails safe.
|
|
1097
|
+
* - it does not skip silently. An expired checkpoint comes back as a refusal
|
|
1098
|
+
* that NAMES the age and the TTL, because the operator's next move (start
|
|
1099
|
+
* fresh) depends on knowing which of the two it hit.
|
|
1100
|
+
*
|
|
1101
|
+
* The clock is injected for the same reason `runPlan`'s is: a rule measured in
|
|
1102
|
+
* months cannot otherwise be tested at all.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {any[]} events the paused run's own spine, parsed, in file order
|
|
1105
|
+
* @param {{now?: () => number, ttlMs?: number}} [opts]
|
|
1106
|
+
* @returns {{ok: boolean, applies: boolean, ageMs: number|null, ttlMs: number, pausedAt: string|null, detail: string|null}}
|
|
1107
|
+
*/
|
|
1108
|
+
export function checkpointAgeGate(events, { now = Date.now, ttlMs = PAUSE_TTL_MS } = {}) {
|
|
1109
|
+
const list = Array.isArray(events) ? events.filter(isObj) : [];
|
|
1110
|
+
const end = list.filter((e) => e.type === 'job-end').at(-1) ?? null;
|
|
1111
|
+
const base = { applies: false, ageMs: null, ttlMs, pausedAt: null, detail: null };
|
|
1112
|
+
if (end?.outcome !== HITL_PAUSE) return { ...base, ok: true };
|
|
1113
|
+
|
|
1114
|
+
// the PAUSE RECORD's own timestamp, and its terminal's as the fallback: the
|
|
1115
|
+
// pause is what the human is answering, so its stamp is when the waiting began
|
|
1116
|
+
const pause = list.filter((e) => e.type === HITL_PAUSE).at(-1) ?? end;
|
|
1117
|
+
const pausedAt = typeof pause.ts === 'string' ? pause.ts : null;
|
|
1118
|
+
const atMs = pausedAt === null ? NaN : Date.parse(pausedAt);
|
|
1119
|
+
if (!Number.isFinite(atMs)) {
|
|
1120
|
+
return {
|
|
1121
|
+
...base,
|
|
1122
|
+
ok: false,
|
|
1123
|
+
applies: true,
|
|
1124
|
+
detail: 'this checkpoint does not record when it paused, so its age is unknown — and an unknown age is not a '
|
|
1125
|
+
+ `young one (F6). The TTL is ${Math.round(ttlMs / 86_400_000)} days; start a fresh run rather than resuming a `
|
|
1126
|
+
+ 'checkpoint nothing can date.',
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const ageMs = Math.max(0, now() - atMs);
|
|
1130
|
+
const days = (/** @type {number} */ ms) => Math.floor(ms / 86_400_000);
|
|
1131
|
+
if (ageMs > ttlMs) {
|
|
1132
|
+
return {
|
|
1133
|
+
...base,
|
|
1134
|
+
ok: false,
|
|
1135
|
+
applies: true,
|
|
1136
|
+
ageMs,
|
|
1137
|
+
pausedAt,
|
|
1138
|
+
detail: `this run paused ${days(ageMs)} days ago and the pause TTL is ${days(ttlMs)} days — the tree, the `
|
|
1139
|
+
+ 'toolchain and the job itself have had two months to move, so the evidence the signer would be answering '
|
|
1140
|
+
+ 'about is no longer the evidence the run produced. Start a fresh run.',
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return { ...base, ok: true, applies: true, ageMs, pausedAt };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
976
1146
|
/**
|
|
977
1147
|
* The PATIENT gate for a resume, which is deliberately NOT the fresh-launch one.
|
|
978
1148
|
*
|
|
@@ -1325,7 +1495,30 @@ export async function runReuse(opts) {
|
|
|
1325
1495
|
costUsd: read.spentUsd, spendComplete: read.spendComplete, wallMs, rounds,
|
|
1326
1496
|
...(trySpecHash ? { specHash: trySpecHash } : {}),
|
|
1327
1497
|
};
|
|
1328
|
-
if (
|
|
1498
|
+
if (outcome === 'already-green') {
|
|
1499
|
+
// A GREEN THAT PREDATES THE RUN mints nothing, and the guard keys on the
|
|
1500
|
+
// TERMINAL rather than on the absence of a plan. `already-green` means the
|
|
1501
|
+
// close was satisfied at the precheck — before this leg wrote a byte — so
|
|
1502
|
+
// whatever plan is on the spine did not produce it, and minting a version
|
|
1503
|
+
// from one would be learning credit for work that did not happen.
|
|
1504
|
+
//
|
|
1505
|
+
// It used to be covered by accident: a cold already-green never drafts, so
|
|
1506
|
+
// `read.plan` was null and the branch below caught it. N4 broke that
|
|
1507
|
+
// accident — a hitl try that PAUSED and came back with the signer's
|
|
1508
|
+
// `accept` lands `already-green` with its predecessor's `plan-accepted`
|
|
1509
|
+
// sitting in the same try window. A rule with no wired detector is prose.
|
|
1510
|
+
bridgeWrites.push({
|
|
1511
|
+
name,
|
|
1512
|
+
action: 'none',
|
|
1513
|
+
file: null,
|
|
1514
|
+
reds: [{
|
|
1515
|
+
code: 'green-predates-run',
|
|
1516
|
+
path: 'outcome',
|
|
1517
|
+
detail: 'the close was already satisfied before this run did anything (already-green) — nothing here earned '
|
|
1518
|
+
+ 'a version, and a plan on the spine did not produce this green',
|
|
1519
|
+
}],
|
|
1520
|
+
});
|
|
1521
|
+
} else if (verdictClass === 'green' && read.plan === null) {
|
|
1329
1522
|
// a green with no plan on the spine cannot mint a version — the artifact that
|
|
1330
1523
|
// inherits is the one that RAN, and there is nothing here to inherit. Reported,
|
|
1331
1524
|
// never faked with a placeholder.
|
|
@@ -1339,6 +1532,26 @@ export async function runReuse(opts) {
|
|
|
1339
1532
|
toolsUsed: [...new Set((read.plan.steps ?? []).flatMap((/** @type {any} */ s) => s.tools ?? []))],
|
|
1340
1533
|
};
|
|
1341
1534
|
row.bridgeWrite = writeGreen(bridge, meta, { ...record, plan: read.plan });
|
|
1535
|
+
} else if (HUMAN_CHECKPOINTS.includes(outcome)) {
|
|
1536
|
+
// A PAUSE IS NOT A RESULT, so the box gets nothing — not a casualty row, not a red,
|
|
1537
|
+
// and the workflow is left exactly as it was. The recipe has not been judged: the
|
|
1538
|
+
// close is not FINISHED rendering its verdict, and the answer that would finish it
|
|
1539
|
+
// is a person's. Filing it as history would put a row on the entry that reads as a
|
|
1540
|
+
// run this workflow failed to carry, and the signer's `accept` moments later would
|
|
1541
|
+
// leave that row standing as evidence against a workflow that in fact greened.
|
|
1542
|
+
// Recorded as an explicit no-write for the already-green branch's reason: a reader
|
|
1543
|
+
// never has to branch on absence.
|
|
1544
|
+
bridgeWrites.push({
|
|
1545
|
+
name,
|
|
1546
|
+
action: 'none',
|
|
1547
|
+
file: null,
|
|
1548
|
+
reds: [{
|
|
1549
|
+
code: 'run-paused-for-decision',
|
|
1550
|
+
path: 'outcome',
|
|
1551
|
+
detail: `the run is waiting on a person (${outcome}) — nothing has been judged yet, so there is no result to `
|
|
1552
|
+
+ 'file against this workflow. The row is written by whatever the resumed run finally earns.',
|
|
1553
|
+
}],
|
|
1554
|
+
});
|
|
1342
1555
|
} else if (bridge) {
|
|
1343
1556
|
// R1: the recipe is NOT edited by a red. `outcome` rides in verbatim — a casualty
|
|
1344
1557
|
// keeps its own name, and only the literal 'red' demotes (D6).
|
|
@@ -1445,6 +1658,31 @@ export async function runReuse(opts) {
|
|
|
1445
1658
|
* @param {any} row @returns {any|null} the terminal, or null to carry on
|
|
1446
1659
|
*/
|
|
1447
1660
|
const hardStop = (row) => {
|
|
1661
|
+
// THE HUMAN HALF, read before the table and off the LIBRARY's own list: a run
|
|
1662
|
+
// waiting on a person is not one of the stops below, which are properties of the
|
|
1663
|
+
// run that would reproduce for $0. This one is the opposite — a try that did real
|
|
1664
|
+
// work and reached the one stage a machine cannot render. Buying the next try (and
|
|
1665
|
+
// then the cold leg) would spend the envelope re-asking a question already put to
|
|
1666
|
+
// someone who has not replied yet, and the second attempt could not be graded any
|
|
1667
|
+
// sooner than the first. The allowance still unspent is a person, so the loop stops
|
|
1668
|
+
// and hands the run back; the checkpoint on disk is untouched and resumable, which
|
|
1669
|
+
// is the runner script's half (N4 §1.6, PAUSE_TTL_MS).
|
|
1670
|
+
if (HUMAN_CHECKPOINTS.includes(row.runOutcome)) {
|
|
1671
|
+
return done(row.runOutcome, {
|
|
1672
|
+
category: row.runOutcome,
|
|
1673
|
+
decision: 'The run reached a stage only a person can render, and it is waiting on that answer. Every further '
|
|
1674
|
+
+ 'try and the cold leg would run the same job to the same question, so the loop stops here rather than '
|
|
1675
|
+
+ 'buying attempts nobody can grade yet. Nothing is lost: the run kept everything it did, and it resumes '
|
|
1676
|
+
+ 'from this checkpoint once the decision is in.',
|
|
1677
|
+
options: [
|
|
1678
|
+
'read the evidence package the pause recorded, then answer it and resume this run',
|
|
1679
|
+
'leave it: an unanswered checkpoint expires on its own once the pause TTL is up',
|
|
1680
|
+
],
|
|
1681
|
+
// NOT re-filed, for `interpreter-red`'s reason: the pause already escalated from
|
|
1682
|
+
// its own site inside the run, under this same name and with the evidence package
|
|
1683
|
+
// attached. A second copy here would count one person's pending decision twice.
|
|
1684
|
+
}, false);
|
|
1685
|
+
}
|
|
1448
1686
|
const decisions = {
|
|
1449
1687
|
'unapproved-spec': {
|
|
1450
1688
|
decision: `No approval record matches the per-try spec (hash ${trySpecHash}). Tightening the caps with an envelope makes a NEW spec version, and a new version is signed, not inherited — nothing was run.`,
|
package/src/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer R — the root: the within-run ratchet (LAYERS.md item 2; design record
|
|
2
|
-
// docs/
|
|
2
|
+
// docs/02-features/2026-07-19-layer-r-design.md). One state that survives attempts
|
|
3
3
|
// inside ONE run so a never-green run stops repeating itself (F21). Role is
|
|
4
4
|
// CONTINUITY ONLY — F39 measured that hand-delivered state buys no conversion;
|
|
5
5
|
// the semantic converter is Layer 2's job, not this module's.
|
package/src/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// smoke, the ONE budget ledger (cap-not-estimate; unpriced is never free, F6),
|
|
4
4
|
// and the decision-ready stops. It composes the plan flow below it and
|
|
5
5
|
// interprets nothing itself.
|
|
6
|
-
// Design record: docs/
|
|
6
|
+
// Design record: docs/02-features/2026-07-12-n2-headless-loop-design.md.
|
|
7
7
|
//
|
|
8
8
|
// The legacy operator-authored `steps[]` path was DELETED 2026-07-26 (PRD
|
|
9
9
|
// v1.32): the drafting call, config-v1, the per-step interpret loops and the
|
|
@@ -15,6 +15,7 @@ import { join } from 'node:path';
|
|
|
15
15
|
import { LiteCtx } from 'litectx';
|
|
16
16
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
17
17
|
import { runPlan } from './planrun.js';
|
|
18
|
+
import { SMOKE_STORE } from './kinds.js';
|
|
18
19
|
|
|
19
20
|
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
20
21
|
|
|
@@ -29,7 +30,7 @@ import { runPlan } from './planrun.js';
|
|
|
29
30
|
*/
|
|
30
31
|
async function primitiveSmoke(workdir) {
|
|
31
32
|
try {
|
|
32
|
-
const lc = new LiteCtx({ root: join(workdir,
|
|
33
|
+
const lc = new LiteCtx({ root: join(workdir, SMOKE_STORE) });
|
|
33
34
|
await lc.remember('smoke-known-answer', 'bareloop primitive smoke: the answer is 42', { kind: 'fact' });
|
|
34
35
|
const hits = await lc.recall('bareloop primitive smoke known answer', { kind: 'fact', n: 5, body: true });
|
|
35
36
|
const ok = hits.some((h) => String(h.body ?? '').includes('the answer is 42'));
|
|
@@ -139,12 +140,18 @@ async function primitiveSmoke(workdir) {
|
|
|
139
140
|
* a manufactured-fixation probe). `true` is the ON/experimental arm; the
|
|
140
141
|
* default is the OFF arm. (Shell-owned seam, same doctrine as the provider
|
|
141
142
|
* binding.)
|
|
143
|
+
* @param {{decision: string, text?: string}|null} [opts.humanRuling] N4 — the SIGNER's
|
|
144
|
+
* answer at a hitl pause (accept | rerun <text> | pause), carried by the leg that
|
|
145
|
+
* RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
|
|
146
|
+
* ordinary run; never authored and never defaulted (a defaulted answer to "is this
|
|
147
|
+
* done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
|
|
142
148
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
143
149
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
144
150
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
145
|
-
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
151
|
+
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
152
|
+
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
146
153
|
*/
|
|
147
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null }) {
|
|
154
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null }) {
|
|
148
155
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
149
156
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
150
157
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -282,7 +289,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
282
289
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
283
290
|
{
|
|
284
291
|
const outcome = await runPlan(job, {
|
|
285
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch,
|
|
292
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling,
|
|
286
293
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
287
294
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
288
295
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/selection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Layer 3 — D3's DISPLAY half: the workflow LISTING, and the prompt that asks for a
|
|
2
|
-
// pick (design record docs/
|
|
2
|
+
// pick (design record docs/02-features/2026-08-01-layer-3-reuse-design.md, D3).
|
|
3
3
|
//
|
|
4
4
|
// hamr's ruling, verbatim: *"i think there should be a listing for workflows names that
|
|
5
5
|
// user can choose or auto-choose, or do we want llm to always decide (we still need to
|
package/src/text.js
CHANGED
|
@@ -70,6 +70,85 @@ export function priceOf(r) {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The F6 tally over a list of PRICED CALLS — `priceOf`'s per-call read, summed.
|
|
75
|
+
*
|
|
76
|
+
* `knownUsd` carries the priced half explicitly and `costUsd` is the honest
|
|
77
|
+
* total: null the moment anything went unpriced, never the priced half wearing a
|
|
78
|
+
* complete coat. An EMPTY list is a complete $0 and not an unknown — nothing
|
|
79
|
+
* spent is a number somebody can act on.
|
|
80
|
+
* @param {{costUsd: number|null, unpricedRounds?: number}[]|null|undefined} entries
|
|
81
|
+
* @returns {{costUsd: number|null, knownUsd: number, spendComplete: boolean,
|
|
82
|
+
* nullCostCalls: number, unpricedRounds: number}}
|
|
83
|
+
*/
|
|
84
|
+
export function tallyCalls(entries) {
|
|
85
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
86
|
+
const known = list.reduce((a, e) => a + (typeof e.costUsd === 'number' ? e.costUsd : 0), 0);
|
|
87
|
+
const nullCostCalls = list.filter((e) => e.costUsd === null).length;
|
|
88
|
+
const unpricedRounds = list.reduce((a, e) => a + (e.unpricedRounds || 0), 0);
|
|
89
|
+
const spendComplete = nullCostCalls === 0 && unpricedRounds === 0;
|
|
90
|
+
return {
|
|
91
|
+
costUsd: spendComplete ? Number(known.toFixed(6)) : null,
|
|
92
|
+
knownUsd: Number(known.toFixed(6)),
|
|
93
|
+
spendComplete,
|
|
94
|
+
nullCostCalls,
|
|
95
|
+
unpricedRounds,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* THE ONE MONEY-CEILING PREDICATE, for the same reason `priceOf` above is one:
|
|
101
|
+
* the authoring pipeline meters two populations of paid call in two different
|
|
102
|
+
* accumulators (the survey's `calls[]`, the declaration loop's cost book), and
|
|
103
|
+
* two hand-spelled answers to "am I over the ceiling?" is two instruments.
|
|
104
|
+
*
|
|
105
|
+
* It is asked BETWEEN metered calls and never inside one. A cap that binds
|
|
106
|
+
* mid-call kills the row before it can be graded — F45 measured that as 3 of 7
|
|
107
|
+
* launches lost and $5.82 unreadable.
|
|
108
|
+
*
|
|
109
|
+
* Three answers, and the order between the last two is load-bearing:
|
|
110
|
+
* - NO CEILING → null, always. An absent cap is UNBOUNDED, and unbounded is a
|
|
111
|
+
* visible operator choice, never a defaulted number nobody set (the
|
|
112
|
+
* `maxWallMs` precedent). There is nothing to enforce, so unpriced spend
|
|
113
|
+
* here is merely reported.
|
|
114
|
+
* - KNOWN SPEND AT OR OVER THE CEILING → `cap-halt`, even when the total is
|
|
115
|
+
* unknown. The breach is CERTAIN on the priced half alone — whatever the
|
|
116
|
+
* unpriced remainder turns out to be, it can only be larger — so naming this
|
|
117
|
+
* `pricing-red` would send the operator to fix a meter when the fact is that
|
|
118
|
+
* the money is gone. At-or-over rather than over: the next call is the one
|
|
119
|
+
* that breaches, and it is the one being decided.
|
|
120
|
+
* - SPEND THAT CANNOT BE KNOWN → `pricing-red`. `?? 0` launders unknown into
|
|
121
|
+
* $0 (F6) and a ceiling enforced against a laundered floor is not enforced
|
|
122
|
+
* at all. If the spend cannot be seen the cap cannot govern it, and that is
|
|
123
|
+
* its own stop rather than a silent pass.
|
|
124
|
+
*
|
|
125
|
+
* A MALFORMED ceiling is the one input it refuses rather than answers: `null` and
|
|
126
|
+
* an absent field mean UNBOUNDED, and they are the ONLY way to get there. Anything
|
|
127
|
+
* else that is not a finite number (`'2.50'`, `NaN`, `Infinity`) reading as
|
|
128
|
+
* unbounded would be F6's laundering in its ceiling form — a caller who set a cap
|
|
129
|
+
* getting a paid pipeline with nothing enforcing it, which is the exact collapse
|
|
130
|
+
* the ceiling exists to prevent, wearing the caller's own typo (PRD v1.62). The
|
|
131
|
+
* CLI's `parseCeiling` guards the FLAG; this guards the LIBRARY, and `authorClose`
|
|
132
|
+
* / `runAuthorScout` / `authorCloseForJob` are documented adopter seams. It THROWS
|
|
133
|
+
* rather than clamping, unlike the attempt caps beside it, because those axes have
|
|
134
|
+
* a safe direction to clamp toward and this one does not: every candidate default
|
|
135
|
+
* is either a number nobody set or the unbounded run the caller just declined.
|
|
136
|
+
* @param {{ceilingUsd: number|null|undefined, knownUsd: number, spendComplete: boolean}} o
|
|
137
|
+
* @returns {'cap-halt'|'pricing-red'|null}
|
|
138
|
+
* @throws {Error} when the ceiling is neither absent nor a finite number
|
|
139
|
+
*/
|
|
140
|
+
export function capStop({ ceilingUsd, knownUsd, spendComplete }) {
|
|
141
|
+
if (ceilingUsd === null || ceilingUsd === undefined) return null;
|
|
142
|
+
if (typeof ceilingUsd !== 'number' || !Number.isFinite(ceilingUsd)) {
|
|
143
|
+
throw new Error(`capStop: a money ceiling must be a finite number of dollars or null — got ${typeof ceilingUsd} `
|
|
144
|
+
+ `${String(ceilingUsd)}. A malformed ceiling is an ERROR, never a silent fall back to UNBOUNDED: omit it, or `
|
|
145
|
+
+ 'pass an explicit null, to run unbounded — that is a stated operator choice and never a typo arrived at');
|
|
146
|
+
}
|
|
147
|
+
if (knownUsd >= ceilingUsd) return 'cap-halt';
|
|
148
|
+
if (!spendComplete) return 'pricing-red';
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
73
152
|
// ── THE RAW MODEL OUTPUT, AS A PERSISTED RECORD ─────────────────────────────
|
|
74
153
|
//
|
|
75
154
|
// Run mslhn707 died on a survey that broke JSON at position 1339 and left NO
|
package/src/tools.js
CHANGED
|
@@ -31,6 +31,7 @@ import { extractArtifact, priceOf } from './text.js';
|
|
|
31
31
|
// caller can forget to pass, and this leak has to be inexpressible, not optional.
|
|
32
32
|
// Same ONE inventory the spine channel and the close output ride through.
|
|
33
33
|
import { redactSecrets } from './validate.js';
|
|
34
|
+
import { GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
|
|
34
35
|
|
|
35
36
|
/** @typedef {{body?: string|null, text?: string|null}} RecallHit litectx recall hit — body present only with `{body: true}` */
|
|
36
37
|
|
|
@@ -45,6 +46,16 @@ const PERSONA = 'You are a senior engineer. Reply with ONLY the complete content
|
|
|
45
46
|
// cheap, wrong attempt the rational move — which is exactly what the loop wants.
|
|
46
47
|
export const PERSONA_TOOLS = 'You are a senior engineer working in a repository through file tools. '
|
|
47
48
|
+ 'ALWAYS use absolute paths — relative paths resolve against the process, not the repository, and will be denied. '
|
|
49
|
+
// F98's residual: the fence ALREADY denies these (planrun's Gate deny list), and the
|
|
50
|
+
// spine is written outside the patient entirely — but nothing ever SAID so, and a
|
|
51
|
+
// denial is a wall while a stated law is a map. The way a worker learned the rule was
|
|
52
|
+
// by spending rounds of a bounded attempt probing bookkeeping files, and the rounds a
|
|
53
|
+
// probe costs are rounds the fix does not get. Registered here, beside the other fence
|
|
54
|
+
// fact a worker cannot infer, so it renders for every worker on every grant.
|
|
55
|
+
// the names are SPELLED FROM the one home (`ARBITER_BOOK_STORES`) so the stated
|
|
56
|
+
// law can never drift from the fence that enforces it
|
|
57
|
+
+ `The repository root also holds the arbiter's own books — ${GATE_AUDIT_FILE}, ${ARBITER_BOOK_STORES.join(', ')}, and this run's spine. `
|
|
58
|
+
+ 'They are ALWAYS denied and record how you are being judged, never anything about the task: never read them, and never try to route around the denial. '
|
|
48
59
|
+ 'You are ONE attempt inside an automated loop: when you finish, a test suite runs and, if it still fails, you are called again with its output. '
|
|
49
60
|
+ 'So do not try to be certain before acting. Read only what you need to form your best hypothesis, make the change with the write tool, and stop. '
|
|
50
61
|
+ 'A wrong cheap attempt is corrected by the next round; exhaustive reading is not — every file you read is re-sent on every later round and the run has a hard budget it can exhaust before you ever write. '
|