bareloop 0.7.0 → 0.8.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 +283 -0
- package/README.md +4 -2
- package/bareloop.context.md +156 -18
- package/package.json +1 -1
- package/src/exits.js +8 -2
- package/src/job.js +43 -6
- package/src/ledger.js +57 -9
- package/src/plan.js +31 -100
- package/src/planrun.js +318 -62
- package/src/ralph.js +8 -1
- package/src/reuse.js +67 -4
- package/src/run.js +21 -2
- package/src/validate.js +121 -0
- package/types/exits.d.ts +14 -0
- package/types/job.d.ts +13 -2
- package/types/plan.d.ts +23 -38
- package/types/planrun.d.ts +116 -16
- package/types/ralph.d.ts +16 -0
- package/types/reuse.d.ts +5 -1
- package/types/run.d.ts +14 -1
- package/types/validate.d.ts +50 -0
package/src/reuse.js
CHANGED
|
@@ -594,7 +594,11 @@ function readGradeSeed(seen) {
|
|
|
594
594
|
* **The declared fold, not a re-derivation.** A restarted try's own `try-start`
|
|
595
595
|
* carries the fold it inherited (`priorSpentUsd`/`priorWallMs`), so a resume OF a
|
|
596
596
|
* resume adds only its own new rounds. Re-deriving from the whole file would bill an
|
|
597
|
-
* abandoned attempt twice.
|
|
597
|
+
* abandoned attempt twice. The REPLAN ledger (`priorReplans`/`priorReplanGrantUsed`)
|
|
598
|
+
* folds on this same mechanism and deliberately not on `readGradeSeed`'s: a grade seed
|
|
599
|
+
* feeds a readout, so its documented one-leg-per-resume shortening can only under-claim
|
|
600
|
+
* a direction, while an under-claimed CEILING is a refilled allowance and every kill
|
|
601
|
+
* would buy one (PRD v1.12 — the replan bound is the RUN's).
|
|
598
602
|
*
|
|
599
603
|
* **KNOWN LIMIT, stated rather than discovered later.** A call that was killed BEFORE it
|
|
600
604
|
* returned left no event at all — no `worker-round`, no `selection-result` — so money it
|
|
@@ -688,6 +692,19 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
688
692
|
// declaration existed, or a try nobody resumed — both are honestly zero.
|
|
689
693
|
declaredRounds: !('priorRounds' in ev) ? 0
|
|
690
694
|
: (typeof ev.priorRounds === 'number' && Number.isFinite(ev.priorRounds) && ev.priorRounds >= 0 ? ev.priorRounds : null),
|
|
695
|
+
// the REPLAN ledger this attempt was already carrying, on the same declared-fold
|
|
696
|
+
// mechanism and with the same `> 0` belt as the money. It has to fold this way
|
|
697
|
+
// rather than the grade seed's way: `readGradeSeed`'s chain shortens by one leg
|
|
698
|
+
// per resume, which under-claims — harmless for a readout, and for a CEILING an
|
|
699
|
+
// under-claim IS a refilled allowance, one per kill (PRD v1.12).
|
|
700
|
+
declaredReplans: typeof ev.priorReplans === 'number' && Number.isFinite(ev.priorReplans) && ev.priorReplans > 0 ? Math.floor(ev.priorReplans) : 0,
|
|
701
|
+
// the F85-C latch rides BESIDE the count, never derived from it: a chain can have
|
|
702
|
+
// spent the ordinary ceiling with the arbiter's one extra still unearned, and
|
|
703
|
+
// deriving one from the other would collapse those two states into one
|
|
704
|
+
declaredGrantUsed: ev.priorReplanGrantUsed === true,
|
|
705
|
+
// this window's OWN replans, counted from the records the runner emits — the same
|
|
706
|
+
// `replan` event a human reads, so the ledger and the log cannot disagree
|
|
707
|
+
windowReplans: 0, windowGrantUsed: false,
|
|
691
708
|
roundsUsd: 0, roundsComplete: true, r1Written: false, jobEnd: null,
|
|
692
709
|
/** @type {any[]} the window's own events, so the SAME `readTry` the live loop uses
|
|
693
710
|
* reads a graded-but-unrecorded try — never a second reader spelling it differently */
|
|
@@ -745,6 +762,16 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
745
762
|
open.jobEnd = ev;
|
|
746
763
|
} else if (ev.type === 'bridge-write') {
|
|
747
764
|
open.r1Written = true;
|
|
765
|
+
} else if (ev.type === 'replan') {
|
|
766
|
+
// ONE record per replan the runner drafts, so counting them IS the leg's ledger
|
|
767
|
+
// — never re-derived from `plan-accepted` (a resumed leg emits one of those for a
|
|
768
|
+
// plan it RELOADED, which would read as a redraft nobody paid for).
|
|
769
|
+
open.windowReplans += 1;
|
|
770
|
+
// the arbiter stamps `granted` only on the extra it granted past the ordinary
|
|
771
|
+
// ceiling, and stamps nothing on the ordinary one — so PRESENCE is the whole
|
|
772
|
+
// signal, and reading it by presence keeps this reader honest about a reading it
|
|
773
|
+
// does not itself make (never re-deciding whether the trend deserved it).
|
|
774
|
+
if (ev.granted !== undefined) open.windowGrantUsed = true;
|
|
748
775
|
} else if (ev.type === 'try-end') {
|
|
749
776
|
completed.push({ ...ev, seq: undefined, ts: undefined, inherited: true });
|
|
750
777
|
if (open.bridge) tried.push(open.bridge);
|
|
@@ -815,6 +842,19 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
815
842
|
// restart already re-graded that tree. Counting both would enter the same
|
|
816
843
|
// reading twice and flatten a chain that moved.
|
|
817
844
|
grades: readGradeSeed(open.seen),
|
|
845
|
+
// the REPLAN ledger, folded like the money one line below and deliberately
|
|
846
|
+
// NOT like the grades one line above. A grade seed feeds a READOUT and its
|
|
847
|
+
// documented shortening chain can only under-claim a direction; this feeds a
|
|
848
|
+
// BOUND, where under-claiming is a refilled allowance and every kill would buy
|
|
849
|
+
// one (PRD v1.12: unlimited replanning launders thrash as adaptation). So it
|
|
850
|
+
// reads the declared fold plus THIS window's own records — the same arithmetic
|
|
851
|
+
// as `priorSpentUsd`, which is what makes a resume of a resume inherit leg 1.
|
|
852
|
+
//
|
|
853
|
+
// The abandoned windows are not read, for the money's exact reason: the
|
|
854
|
+
// restart DECLARED what it inherited, so counting the window it abandoned
|
|
855
|
+
// would charge the chain twice for one leg's replans.
|
|
856
|
+
replans: open.declaredReplans + open.windowReplans,
|
|
857
|
+
replanGrantUsed: open.declaredGrantUsed || open.windowGrantUsed,
|
|
818
858
|
priorSpentUsd: open.declaredSpentUsd + open.roundsUsd,
|
|
819
859
|
// F83 — was that fold EXACT? Three sources, because `runJob` decides the same
|
|
820
860
|
// question from more than this window can see (src/run.js: `!unpriced &&
|
|
@@ -1017,7 +1057,7 @@ export async function runReuse(opts) {
|
|
|
1017
1057
|
const runid = opts.runid ?? Date.now().toString(36);
|
|
1018
1058
|
const resume = isObj(opts.resume) ? /** @type {any} */ (opts.resume) : null;
|
|
1019
1059
|
/** a try that inherits nothing: the ordinary, non-resumed attempt */
|
|
1020
|
-
const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]) };
|
|
1060
|
+
const NO_PRIOR = { spentUsd: 0, wallMs: 0, spendComplete: true, rounds: /** @type {number|null} */ (0), grades: /** @type {any[]} */ ([]), replans: 0, replanGrantUsed: false };
|
|
1021
1061
|
|
|
1022
1062
|
/** @type {any[]} */
|
|
1023
1063
|
const tries = [];
|
|
@@ -1113,7 +1153,7 @@ export async function runReuse(opts) {
|
|
|
1113
1153
|
|
|
1114
1154
|
/** the shared per-try execution: run the job, read the spine, write the box (R1).
|
|
1115
1155
|
* @param {any|null} bridge @param {number} n
|
|
1116
|
-
* @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[]}} [prior]
|
|
1156
|
+
* @param {{spentUsd: number, wallMs: number, spendComplete: boolean, rounds: number|null, grades: any[], replans: number, replanGrantUsed: boolean}} [prior]
|
|
1117
1157
|
* RESUME — what a KILLED attempt of this same try already consumed, and what it
|
|
1118
1158
|
* already measured. Money and wall are FOLDED into the attempt (`runJob`'s own ledger
|
|
1119
1159
|
* and clock start partly spent) rather than shrinking the caps, so the signed per-try
|
|
@@ -1121,7 +1161,10 @@ export async function runReuse(opts) {
|
|
|
1121
1161
|
* way but on the ROW alone (nothing enforces a round bound here), and `null` — an
|
|
1122
1162
|
* attempt whose turns could not be counted — is reported, never folded. `grades` are
|
|
1123
1163
|
* the close readings that leg recorded, handed on as the run trend's BASELINES: they
|
|
1124
|
-
* feed the HALT READOUT alone and spend none of this attempt's bounds.
|
|
1164
|
+
* feed the HALT READOUT alone and spend none of this attempt's bounds. `replans` /
|
|
1165
|
+
* `replanGrantUsed` are the opposite kind of inheritance and sit here for that
|
|
1166
|
+
* contrast: they SPEND a bound (the plan flow's replan ceiling), because that ceiling
|
|
1167
|
+
* is the RUN's by doctrine and a fresh one per kill is the creep v1.12 forbids.
|
|
1125
1168
|
* @param {any} [seed] RESUME (v2) — WHERE that attempt was killed (`readResume`'s
|
|
1126
1169
|
* `restart.seed`): the plan it accepted and the steps it finished. The fold says how
|
|
1127
1170
|
* much of the try's allowance is gone; this says how much of its WORK is done, so the
|
|
@@ -1142,6 +1185,15 @@ export async function runReuse(opts) {
|
|
|
1142
1185
|
// means "a spine older than this field", and a reader that cannot tell that from a
|
|
1143
1186
|
// predecessor's honest unknown would launder the unknown into a zero (F6).
|
|
1144
1187
|
...(resumed ? { priorSpentUsd: prior.spentUsd, priorWallMs: prior.wallMs, priorSpendComplete: prior.spendComplete, priorRounds: prior.rounds, resumedFrom: resume?.fromRunid ?? runid } : {}),
|
|
1188
|
+
// the REPLAN ledger, declared on the same record and for the same reason as the
|
|
1189
|
+
// money: the next reader adds only its own window's `replan` records to this
|
|
1190
|
+
// number, so a chain of resumes folds once instead of restarting the ceiling at
|
|
1191
|
+
// whichever leg it happens to be reading (PRD v1.12 — the bound is the RUN's).
|
|
1192
|
+
// Declared independently of `resumed`, which is a MONEY/WALL test: a leg can
|
|
1193
|
+
// replan without buying a round the fold would notice, and gating the ledger on
|
|
1194
|
+
// the wallet would drop it exactly there. Only when there is one to state — a
|
|
1195
|
+
// decorative zero is indistinguishable from a leg nobody folded.
|
|
1196
|
+
...(prior.replans > 0 ? { priorReplans: prior.replans, priorReplanGrantUsed: prior.replanGrantUsed } : {}),
|
|
1145
1197
|
// WHERE it picks up, on the row's own record: a try that restarts at step 3 of 4 is
|
|
1146
1198
|
// a materially different attempt from one that restarts at its beginning, and a
|
|
1147
1199
|
// reader must not have to infer which happened from the absence of a scout.
|
|
@@ -1173,6 +1225,11 @@ export async function runReuse(opts) {
|
|
|
1173
1225
|
// restarted leg would report `flat` on a run that was converging. Only when there
|
|
1174
1226
|
// ARE grades: an empty array is the cold path, and the cold path stays identical.
|
|
1175
1227
|
...(prior.grades.length ? { resumeGrades: prior.grades } : {}),
|
|
1228
|
+
// and the replan LEDGER. Same omission class as the two above, with a sharper
|
|
1229
|
+
// cost: a grade seam that never arrives makes a readout say `flat`, while a
|
|
1230
|
+
// ceiling that never arrives is spent by the dead leg and fresh in this one —
|
|
1231
|
+
// a bound the operator signed, widened by a kill (PRD v1.12).
|
|
1232
|
+
...(prior.replans > 0 ? { resumeReplans: { count: prior.replans, grantUsed: prior.replanGrantUsed } } : {}),
|
|
1176
1233
|
...(seed ? { resumeSeed: seed } : {}),
|
|
1177
1234
|
bridge,
|
|
1178
1235
|
});
|
|
@@ -1446,6 +1503,12 @@ export async function runReuse(opts) {
|
|
|
1446
1503
|
// this run make progress" and "is this loop out of ideas" are two questions,
|
|
1447
1504
|
// and one instrument answering both is how they come to disagree.
|
|
1448
1505
|
grades: Array.isArray(rs.grades) ? rs.grades : [],
|
|
1506
|
+
// the replan LEDGER the chain has spent — belted like the money and NOT like
|
|
1507
|
+
// the grades, because this one seeds a BOUND: a garbage or negative figure
|
|
1508
|
+
// that read through as a widening is the ceiling being refilled by a corrupt
|
|
1509
|
+
// spine (PRD v1.12). The latch rides beside the count, never derived from it.
|
|
1510
|
+
replans: typeof rs.replans === 'number' && Number.isFinite(rs.replans) && rs.replans > 0 ? Math.floor(rs.replans) : 0,
|
|
1511
|
+
replanGrantUsed: rs.replanGrantUsed === true,
|
|
1449
1512
|
}
|
|
1450
1513
|
: NO_PRIOR;
|
|
1451
1514
|
const remainingCapUsd = envelope.perTryBudgetUsd - prior.spentUsd;
|
package/src/run.js
CHANGED
|
@@ -116,6 +116,15 @@ async function primitiveSmoke(workdir) {
|
|
|
116
116
|
* chain, not just the leg: without them a leg that re-grades an unchanged tree reports
|
|
117
117
|
* `flat` — "revise the goal" — on a run that was converging when the money ran out.
|
|
118
118
|
* Counts and stage names only; no close bytes cross this seam. Absent is the cold path.
|
|
119
|
+
* @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
|
|
120
|
+
* replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
|
|
121
|
+
* `restart.replanGrantUsed`), forwarded to the plan flow, which seeds its ceiling with
|
|
122
|
+
* it. Its sibling above feeds a READOUT; this one feeds a BOUND, and that is the whole
|
|
123
|
+
* difference: `replanned`/`varianceGrantUsed` are locals in `runPlan`, so without this
|
|
124
|
+
* every kill hands the next leg a fresh allowance of an allowance PRD v1.12 makes the
|
|
125
|
+
* RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
|
|
126
|
+
* run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
|
|
127
|
+
* already sets, so a resume OF a resume folds once. Absent is the cold path.
|
|
119
128
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
120
129
|
* territory, threaded to the plan flow. Defaults OFF
|
|
121
130
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -128,7 +137,7 @@ async function primitiveSmoke(workdir) {
|
|
|
128
137
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
|
|
129
138
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
|
|
130
139
|
*/
|
|
131
|
-
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 = [] }) {
|
|
140
|
+
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 }) {
|
|
132
141
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
133
142
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
134
143
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -204,6 +213,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
204
213
|
// it inherited, and it is the only field that can carry it forward.
|
|
205
214
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
206
215
|
...(typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? { priorWallMs } : {}),
|
|
216
|
+
// the REPLAN ledger, on the same declaration and for the same reason — a chain of
|
|
217
|
+
// resumes must fold once. This is the DIRECT-spine half of it: `readResume`'s
|
|
218
|
+
// `direct` mode opens its one implicit window on `job-start` and reads the fold off
|
|
219
|
+
// these very fields, so a U-path run resumed twice inherits leg 1's replans rather
|
|
220
|
+
// than restarting the ceiling at leg 2's window (PRD v1.12). Emitted only when there
|
|
221
|
+
// is one — a decorative `0` is indistinguishable from a run nobody folded, and the
|
|
222
|
+
// grant latch travels beside the count rather than being derived from it.
|
|
223
|
+
...(typeof resumeReplans?.count === 'number' && Number.isFinite(resumeReplans.count) && resumeReplans.count > 0
|
|
224
|
+
? { priorReplans: Math.floor(resumeReplans.count), priorReplanGrantUsed: resumeReplans.grantUsed === true }
|
|
225
|
+
: {}),
|
|
207
226
|
});
|
|
208
227
|
|
|
209
228
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
@@ -256,7 +275,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
256
275
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
257
276
|
{
|
|
258
277
|
const outcome = await runPlan(job, {
|
|
259
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades,
|
|
278
|
+
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans,
|
|
260
279
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
261
280
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
262
281
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/validate.js
CHANGED
|
@@ -155,3 +155,124 @@ export function sweepSecretLiterals(root, red) {
|
|
|
155
155
|
}
|
|
156
156
|
})(root, '');
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── the catastrophic-backtracking reject (F49) ────────────────────────────────
|
|
160
|
+
// Housed here, with the other primitives BOTH validators share, because THREE
|
|
161
|
+
// regex fields reach an untimed evaluator and they are split across the two
|
|
162
|
+
// documents: the agent's `artifact-written.pattern` (plan.js) and the
|
|
163
|
+
// operator's `judged.pattern` + `gapKeep` (job.js). plan.js already imports
|
|
164
|
+
// job.js, so job.js cannot import plan.js — and a second copy here would be the
|
|
165
|
+
// drift class SECRET_PATTERNS exists to prevent: a shape one side rejects and
|
|
166
|
+
// the other admits. ONE inventory, imported by both.
|
|
167
|
+
|
|
168
|
+
/** Length of an UNBOUNDED quantifier token at `src[i]` (`*`, `+`, or `{n,}`,
|
|
169
|
+
* plus an optional trailing lazy `?`), or 0. Bounded forms (`?`, `{n}`,
|
|
170
|
+
* `{n,m}`) return 0: they cannot drive exponential backtracking (a bounded
|
|
171
|
+
* outer repeat is polynomial at worst, and the body is self-authored — F49).
|
|
172
|
+
* @param {string} src @param {number} i */
|
|
173
|
+
function unboundedQuantLen(src, i) {
|
|
174
|
+
let len = src[i] === '*' || src[i] === '+' ? 1 : 0;
|
|
175
|
+
if (!len && src[i] === '{') { const m = /^\{\d+,\}/.exec(src.slice(i)); if (m) len = m[0].length; }
|
|
176
|
+
if (len && src[i + len] === '?') len++; // the lazy modifier is part of the same token
|
|
177
|
+
return len;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
182
|
+
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
183
|
+
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
184
|
+
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
185
|
+
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
186
|
+
* pattern can hang `RegExp.test` for seconds on a short crafted body, and THREE
|
|
187
|
+
* fields feed it to an evaluator with NO timeout — one per document:
|
|
188
|
+
* - the AGENT's `artifact-written.pattern`, run by `evalExits` (F49, the
|
|
189
|
+
* original site). Self-DoS only: the agent authors both the pattern and
|
|
190
|
+
* (via the worker) the artifact, so a hang burns only its own run's
|
|
191
|
+
* wall-clock, and there is no arbiter compromise — it cannot escape the
|
|
192
|
+
* fence, forge a green, or leak a secret.
|
|
193
|
+
* - the OPERATOR's `judged.pattern` (`runClose`) and `gapKeep` (`boundGap`),
|
|
194
|
+
* compiled and run against close output in BARELOOP'S OWN PROCESS. These
|
|
195
|
+
* are worse than an operator self-DoS, and the reason is F67's lesson: the
|
|
196
|
+
* regex blocks the MAIN EVENT LOOP, so the in-process stall fuse — a timer
|
|
197
|
+
* in that same loop — cannot fire. The run dies to the OUTSIDE watchdog and
|
|
198
|
+
* is read as a model stall, so a bad regex in a SIGNED spec presents as a
|
|
199
|
+
* provider problem. MEASURED: `boundGap(stream, '(a+)+$')` on a 40-char
|
|
200
|
+
* body does not finish in 20s.
|
|
201
|
+
* So the reject targets the dominant exponential class and fails it as a
|
|
202
|
+
* mechanical gap at the validation gate, before any tokens burn. A full ReDoS
|
|
203
|
+
* analyzer needs a real regex engine (an external native dep we do not take for
|
|
204
|
+
* a LOW issue); exotic overlapping-alternation blowup is out of scope by
|
|
205
|
+
* decision (PARKED to the close-authoring rung — PRD v1.55).
|
|
206
|
+
*
|
|
207
|
+
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
208
|
+
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
209
|
+
* detected — there is no inner QUANTIFIER to find, so the shape scan cannot
|
|
210
|
+
* see it; the blowup comes from overlapping alternation BRANCHES under a
|
|
211
|
+
* repeat. A genuine limit of the approach, not a bug in it. Widening the
|
|
212
|
+
* detector changes which SIGNED specs are admissible, which is
|
|
213
|
+
* arbiter-adjacent — parked, not chased (PRD v1.55).
|
|
214
|
+
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
215
|
+
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
216
|
+
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
217
|
+
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
218
|
+
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
219
|
+
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
220
|
+
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
221
|
+
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
222
|
+
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
223
|
+
*
|
|
224
|
+
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
225
|
+
* so this scan assumes valid, balanced JS regex syntax.
|
|
226
|
+
* @param {string} src a compiled regex source string
|
|
227
|
+
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
228
|
+
*/
|
|
229
|
+
export function hasNestedQuantifier(src) {
|
|
230
|
+
/** @type {{ quant: boolean }[]} */
|
|
231
|
+
const stack = [];
|
|
232
|
+
for (let i = 0; i < src.length; i++) {
|
|
233
|
+
const c = src[i];
|
|
234
|
+
if (c === '\\') { i++; continue; } // escaped atom — the next char is a literal
|
|
235
|
+
if (c === '[') { // character class: quantifier chars inside are literals
|
|
236
|
+
i++;
|
|
237
|
+
if (src[i] === '^') i++;
|
|
238
|
+
// NO POSIX leading-`]`-is-a-literal rule: JS is not POSIX here. `[]` is the
|
|
239
|
+
// EMPTY class (matches nothing) and `[^]` the any-char idiom — both close at
|
|
240
|
+
// that first `]`. Skipping it ran the scan past the class's real end and
|
|
241
|
+
// swallowed every quantifier after it, so `x[^](a+)+$` read as safe while
|
|
242
|
+
// RegExp.test on it does not finish in 15s on a 31-char body. A false
|
|
243
|
+
// NEGATIVE is the dangerous direction (F49), so the scan follows JS.
|
|
244
|
+
//
|
|
245
|
+
// Honest limit on the monotonicity claim: this is a PARSE correction, not
|
|
246
|
+
// the "add rejections only" tightening F49's rule contemplates, and it is
|
|
247
|
+
// not strictly rejection-monotonic. Measured over 14,862 compilable
|
|
248
|
+
// generated patterns (2,369 previously rejected): 2 rejections disappear,
|
|
249
|
+
// both pathological `[]`-bearing shapes whose old `true` came from the very
|
|
250
|
+
// misparse being fixed, and both verified to finish RegExp.test in ~0ms.
|
|
251
|
+
// Every entry of the checked-in REDOS_BAD/GOOD/OVERREJECTED corpus is
|
|
252
|
+
// unchanged. So no real hazard was un-rejected — but the claim is
|
|
253
|
+
// "measured, no hazard lost", never "monotonic by construction".
|
|
254
|
+
while (i < src.length && src[i] !== ']') { if (src[i] === '\\') i++; i++; }
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (c === '(') { stack.push({ quant: false }); continue; }
|
|
258
|
+
if (c === ')') {
|
|
259
|
+
const g = stack.pop();
|
|
260
|
+
const qlen = unboundedQuantLen(src, i + 1);
|
|
261
|
+
if (g && g.quant && qlen) return true; // group repeats unboundedly AND its body did too
|
|
262
|
+
// The group is an unbounded-repeated atom of its parent when it is directly
|
|
263
|
+
// re-quantified (qlen) OR its own body already repeats unboundedly (g.quant):
|
|
264
|
+
// a redundant wrapper — ((a+))+ , (?:(a+))* , (((\d*)))+ — is the SAME
|
|
265
|
+
// exponential class as (a+)+, so an inner repeat must propagate THROUGH the
|
|
266
|
+
// enclosing group or the outer quantifier's close never sees it. Propagation
|
|
267
|
+
// is MONOTONIC (it only ever SETS quant=true → only ever adds rejections): it
|
|
268
|
+
// can widen over-rejection — the named FAIL-SAFE direction — but can never
|
|
269
|
+
// introduce a false negative, which is the dangerous one (F49).
|
|
270
|
+
if ((qlen || (g && g.quant)) && stack.length) stack[stack.length - 1].quant = true;
|
|
271
|
+
i += qlen;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const qlen = unboundedQuantLen(src, i); // a quantifier applying to the preceding atom, inside the current group
|
|
275
|
+
if (qlen) { if (stack.length) stack[stack.length - 1].quant = true; i += qlen - 1; }
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
package/types/exits.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ export function evalExits(exits: Array<Record<string, any>>, { dir, snapshot, ru
|
|
|
36
36
|
pass: boolean;
|
|
37
37
|
results: ExitResult[];
|
|
38
38
|
}>;
|
|
39
|
+
/** The check gap is ALREADY bounded upstream by runClose's boundGap (head +
|
|
40
|
+
* gapKeep block ≤ 8192 + tail ≈ 10KB, with the failing-test NAMES preserved).
|
|
41
|
+
* The evaluator must NOT re-truncate it: a 400-char clamp deletes exactly those
|
|
42
|
+
* `not ok`/`FAILED` names — the mechanical gap the F46 conversion mechanism
|
|
43
|
+
* feeds the worker (F28 reintroduced). This cap is a defensive backstop above
|
|
44
|
+
* boundGap's own envelope: a normal bounded gap passes through intact, and only
|
|
45
|
+
* a pathological seam that returned an unbounded gap would ever be trimmed.
|
|
46
|
+
*
|
|
47
|
+
* Exported for the ONE reason `boundGap`/`GAP_KEEP_TRIM_MARKER` are (src/ralph.js):
|
|
48
|
+
* the replan brief re-bounds this same artifact one seam later (`closeGapBlock`), and
|
|
49
|
+
* a second hand-spelled ceiling there would be a second answer to "how big may a
|
|
50
|
+
* check gap be" — the two would drift on exactly the property that matters, whether
|
|
51
|
+
* the `not ok`/`FAILED` names survive. */
|
|
52
|
+
export const CHECK_GAP_MAX: 12000;
|
|
39
53
|
/**
|
|
40
54
|
* `fault` present means the INSTRUMENT could not judge (unwired snapshot/seam,
|
|
41
55
|
* a check in runClose's forbidden zone) — it carries the runClose verdict name
|
package/types/job.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red — `verb`
|
|
1
|
+
/** @typedef {{code: string, path: string, detail?: string, verb?: string, lib?: string}} Red — `verb` and `lib` ride request-reds as structured data (the ledger keys on them, never on prose).
|
|
2
|
+
* `lib` is the TERRITORY the demand lands against, stamped HERE at the emit site
|
|
3
|
+
* (the typed-lib rule): one code, two catalogues — a locked TOOL verb is demand
|
|
4
|
+
* against the worker-surface package, a locked VERDICT type is demand against
|
|
5
|
+
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
6
|
+
* filed as an upstream bug (the BA-2 misattribution class). */
|
|
2
7
|
/**
|
|
3
8
|
* Validate an operator-owned job spec (`schema: "job-v1"`). Never throws on
|
|
4
9
|
* JSON text or plain parsed data — the ingest contract; a live object with a
|
|
@@ -165,11 +170,17 @@ export const PROVIDERS: readonly string[];
|
|
|
165
170
|
* not duplicated here). */
|
|
166
171
|
export const CONDITION_KEYS: readonly string[];
|
|
167
172
|
/**
|
|
168
|
-
* — `verb`
|
|
173
|
+
* — `verb` and `lib` ride request-reds as structured data (the ledger keys on them, never on prose).
|
|
174
|
+
* `lib` is the TERRITORY the demand lands against, stamped HERE at the emit site
|
|
175
|
+
* (the typed-lib rule): one code, two catalogues — a locked TOOL verb is demand
|
|
176
|
+
* against the worker-surface package, a locked VERDICT type is demand against
|
|
177
|
+
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
178
|
+
* filed as an upstream bug (the BA-2 misattribution class).
|
|
169
179
|
*/
|
|
170
180
|
export type Red = {
|
|
171
181
|
code: string;
|
|
172
182
|
path: string;
|
|
173
183
|
detail?: string;
|
|
174
184
|
verb?: string;
|
|
185
|
+
lib?: string;
|
|
175
186
|
};
|
package/types/plan.d.ts
CHANGED
|
@@ -24,42 +24,6 @@
|
|
|
24
24
|
* @returns {string[]} the offerable scopes, deduped, shallowest-first, capped
|
|
25
25
|
*/
|
|
26
26
|
export function legalScopes(writeScope: string[], dirs?: string[], cap?: number): string[];
|
|
27
|
-
/**
|
|
28
|
-
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
29
|
-
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
30
|
-
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
31
|
-
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
32
|
-
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
33
|
-
* pattern can hang `RegExp.test` for
|
|
34
|
-
* seconds on a short crafted body, and `evalExits` runs the agent-authored
|
|
35
|
-
* `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
|
|
36
|
-
* authors both the pattern and (via the worker) the artifact, so a hang burns
|
|
37
|
-
* only its own run's wall-clock; there is NO arbiter compromise (it cannot
|
|
38
|
-
* escape the fence, forge a green, or leak a secret). So the reject targets the
|
|
39
|
-
* dominant exponential class and fails it as a mechanical gap at the validation
|
|
40
|
-
* gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
|
|
41
|
-
* (an external native dep we do not take for a LOW issue); exotic
|
|
42
|
-
* overlapping-alternation blowup is out of scope by decision.
|
|
43
|
-
*
|
|
44
|
-
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
45
|
-
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
46
|
-
* detected — out of scope (self-DoS only, no arbiter compromise).
|
|
47
|
-
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
48
|
-
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
49
|
-
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
50
|
-
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
51
|
-
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
52
|
-
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
53
|
-
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
54
|
-
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
55
|
-
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
56
|
-
*
|
|
57
|
-
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
58
|
-
* so this scan assumes valid, balanced JS regex syntax.
|
|
59
|
-
* @param {string} src a compiled regex source string
|
|
60
|
-
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
61
|
-
*/
|
|
62
|
-
export function hasNestedQuantifier(src: string): boolean;
|
|
63
27
|
/**
|
|
64
28
|
* The close, as the ordered STAGE LIST the plan flow actually executes (PRD
|
|
65
29
|
* v1.28). A staged close is already that list; the legacy object form is the
|
|
@@ -135,7 +99,6 @@ export const MAX_EXITS_PER_STEP: 2;
|
|
|
135
99
|
/** plan size bound — per-step rounds bound the spend, but an unbounded step
|
|
136
100
|
* COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
|
|
137
101
|
export const MAX_PLAN_STEPS: 8;
|
|
138
|
-
export { WRITE_VERBS };
|
|
139
102
|
/** scope-menu bound: the menu is a PROMPT ingredient on every draft and
|
|
140
103
|
* redraft, so an unbounded repo would price the drafting call by its directory
|
|
141
104
|
* count. Shallow entries survive the cap (see `legalScopes`) because they are
|
|
@@ -145,7 +108,27 @@ export const MAX_SCOPE_MENU: 24;
|
|
|
145
108
|
* construction — the agent picks a tier, never names an arbitrary model string
|
|
146
109
|
* (the tier→model mapping and any effort params are the RUNNER's, per provider).
|
|
147
110
|
* opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
|
|
148
|
-
* never plan-selectable.
|
|
111
|
+
* never plan-selectable.
|
|
112
|
+
*
|
|
113
|
+
* 2026-08-06 — haiku dropped, on hamr's order: "haiku should be dropped to see if
|
|
114
|
+
* it passes, if it's agent poor work or harness, like always." This is a REVERSIBLE
|
|
115
|
+
* ATTRIBUTION PROBE, not a finding that haiku is incapable. What the $0 archive read
|
|
116
|
+
* (186 spines / 71 accepted plans / 255 steps) actually shows is a CONFOUND, in both
|
|
117
|
+
* directions: only 20 steps ever declared a tier at all — haiku 12 (9 of them on a
|
|
118
|
+
* REPLANNED plan, 6 the LAST step of their plan, bounds 3–12 rounds, 2 ever green)
|
|
119
|
+
* against explicitly-declared sonnet 8 (bounds 14–32, 5 green). The planner picks
|
|
120
|
+
* haiku for steps it judges mechanical, and replanned steps sit on runs already in
|
|
121
|
+
* trouble, so neither column is a controlled read. All three bareagent-u runs of
|
|
122
|
+
* 2026-08-06 tiered the REPLANNED step down (haiku/6, haiku/8, haiku/12) and all
|
|
123
|
+
* three failed, against sonnet initial steps at 30–40 rounds — a confound sitting
|
|
124
|
+
* on the programme's most recent capability read. Removing the tier is how it gets
|
|
125
|
+
* attributed: with only one tier expressible, a still-failing replan is agent or
|
|
126
|
+
* harness, not the tier.
|
|
127
|
+
*
|
|
128
|
+
* The `model` FIELD and its validator branch stay: a one-entry menu means a plan (or
|
|
129
|
+
* a stored bridge) declaring `model:"sonnet"` still validates, and restoring haiku is
|
|
130
|
+
* a one-token edit. This narrows only what the AGENT may express — `--model haiku`
|
|
131
|
+
* remains the OPERATOR's probe knob (PRD v1.36), untouched in the runner scripts. */
|
|
149
132
|
export const STEP_MODELS: readonly string[];
|
|
150
133
|
export type Red = {
|
|
151
134
|
code: string;
|
|
@@ -154,3 +137,5 @@ export type Red = {
|
|
|
154
137
|
verb?: string;
|
|
155
138
|
};
|
|
156
139
|
import { WRITE_VERBS } from './job.js';
|
|
140
|
+
import { hasNestedQuantifier } from './validate.js';
|
|
141
|
+
export { WRITE_VERBS, hasNestedQuantifier };
|
package/types/planrun.d.ts
CHANGED
|
@@ -1,21 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* fact behind the prose-prohibition trap (u-msdpuaej, 2026-08-03): a drafter wrote
|
|
4
|
-
* "do not modify any file outside X" into a step whose exit check judges the whole
|
|
5
|
-
* goal, the worker obeyed, and the replanner — handed the gap AND the stall note —
|
|
6
|
-
* rebuilt the same shape, because inferring "the exit reds on files this step never
|
|
7
|
-
* touched" is exactly the semantic leap that fails (F38: mechanical gaps convert,
|
|
8
|
-
* semantic ones stall; 3/3 across the mailbox class). This computes that fact from
|
|
9
|
-
* the two books that already exist: path-like tokens in the gap text (extension
|
|
10
|
-
* required — a bare `step/two` is not a file claim) suffix-matched against the F32
|
|
11
|
-
* write audit's absolute paths. Advisory-only and fail-safe: extraction that finds
|
|
12
|
-
* nothing yields no line, never a guess.
|
|
2
|
+
* This step's own last EXIT results, rendered for the REPLAN brief.
|
|
13
3
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
4
|
+
* The label says exits and not "the close", and the distinction is the whole of one
|
|
5
|
+
* fix: `res.gap` is `lastGap`, the join of EVERY failing exit's detail in exit order.
|
|
6
|
+
* With `MAX_EXITS_PER_STEP` at 2 and the mandatory `tree-changed` pairing on a write
|
|
7
|
+
* step, a step that wrote nothing leads the block with the EVALUATOR's own prose
|
|
8
|
+
* (`0 files changed under src/** …`) — captured verbatim from a real replan prompt,
|
|
9
|
+
* under a label reading "the verification's own output". The close stage's output is
|
|
10
|
+
* one of the details here, never reliably all of them. The payload is left alone: "the
|
|
11
|
+
* step wrote nothing" is exactly what a replanner should be told, and stripping it to
|
|
12
|
+
* make an old label true would trade a real fact for a tidy sentence.
|
|
13
|
+
*
|
|
14
|
+
* u-mshcpdg4: a run one strict error from done, killed with $1.10 and 82 seconds
|
|
15
|
+
* unspent. The close named the remaining work exactly — `src/recurse.js(978,115)`
|
|
16
|
+
* — and the worker read that every attempt. The REPLANNER, which is the component
|
|
17
|
+
* that chooses which files the next plan targets, was handed only the trend line
|
|
18
|
+
* (`still progressing — typecheck 30 → 15 → 15 → 1`): converging, and silent about
|
|
19
|
+
* WHERE. It re-targeted `src/loop.js`, already at zero errors; that step wrote
|
|
20
|
+
* nothing and struck out. F85 gave the brief the trajectory; this gives it the
|
|
21
|
+
* artifact the trajectory is a summary OF.
|
|
22
|
+
*
|
|
23
|
+
* Handed over as TEXT, never as a parsed file list, and the trap is worth naming
|
|
24
|
+
* because the parse is the obvious-looking fix: line 1 of that very gap reads
|
|
25
|
+
* `reports 1 error(s) in src/recurse.js, src/loop.js` — every file in SCOPE — and
|
|
26
|
+
* only line 2 names the culprit. The `never wrote` advisory that used to sit here
|
|
27
|
+
* did exactly that parse, and on this gap it resolved to `src/loop.js` — the file
|
|
28
|
+
* already CLEAN — then said so as a directive beside the artifact that said the
|
|
29
|
+
* opposite. It was deleted with this change (hamr, 2026-08-05): two readers of one
|
|
30
|
+
* question, the parsed one wrong, is the same failure the variance meter just had.
|
|
31
|
+
* Nothing generic can tell a summary line from a detail line, and the
|
|
32
|
+
* shapes differ per close anyway (tsc file:line, pytest test ids, a count close
|
|
33
|
+
* that names no file at all). A model reading the artifact can make that
|
|
34
|
+
* distinction; a regex reproduces the bug. So: no extraction, no digest, no
|
|
35
|
+
* inference — the artifact, bounded and scrubbed.
|
|
36
|
+
*
|
|
37
|
+
* Two boundaries, both reused rather than respelled:
|
|
38
|
+
* - SCRUB. A prompt is an egress point and this repo has ONE secret inventory
|
|
39
|
+
* (`SECRET_PATTERNS` via `redactSecrets`). The gap arrives already scrubbed
|
|
40
|
+
* from `judge`, which makes this defense in depth rather than the only guard —
|
|
41
|
+
* and defense in depth at the egress is exactly where it belongs, because the
|
|
42
|
+
* next caller of this helper will not remember the upstream one.
|
|
43
|
+
* - BOUND, and it is a BACKSTOP rather than an envelope. What arrives here has
|
|
44
|
+
* ALREADY been bounded: `runClose` ran `boundGap` with the stage's own `gapKeep`,
|
|
45
|
+
* which deliberately rescues the `not ok`/`FAILED` names out of the elided middle
|
|
46
|
+
* — the mechanical gap F46's conversion mechanism feeds the worker. Re-bounding
|
|
47
|
+
* that with a fresh 400/1500 envelope deletes exactly those names a second time,
|
|
48
|
+
* which is F28 reintroduced; measured on a 120-red-line close output, the first
|
|
49
|
+
* bound rescued 78 names and this one dropped 12 of them. src/exits.js states the
|
|
50
|
+
* same rule for the same artifact one seam earlier, and this now uses ITS number:
|
|
51
|
+
* `CHECK_GAP_MAX`, imported rather than respelled, so the two seams cannot drift
|
|
52
|
+
* on how big a check gap may be. Under it the gap passes through VERBATIM.
|
|
53
|
+
*
|
|
54
|
+
* A backstop is still needed, and the headroom is why the ceiling is not smaller.
|
|
55
|
+
* `res.gap` is `lastGap`, the JOIN of up to `MAX_EXITS_PER_STEP` failing exit
|
|
56
|
+
* details, and the check-passes branch slices EACH at `CHECK_GAP_MAX` — so what
|
|
57
|
+
* arrives is bounded per detail, never in total. Measured, the real maximum is
|
|
58
|
+
* comfortably under: `runClose`'s own envelope tops out at ~10.1KB (400 head +
|
|
59
|
+
* 8192 keep + 1500 tail + labels), and joined with a `tree-changed` line that is
|
|
60
|
+
* ~10.2KB, so the shipped path passes through untouched with ~1.8KB to spare. The
|
|
61
|
+
* backstop therefore fires only where src/exits.js says it should: a seam that
|
|
62
|
+
* returned something never bounded at all.
|
|
63
|
+
*
|
|
64
|
+
* Over the line `boundGap` runs with `REPLAN_GAP_KEEP`, the same envelope the
|
|
65
|
+
* close path uses, and every trim announces itself (F28). Note what that keep-set
|
|
66
|
+
* cannot do: `\S` matches every non-blank line, so the keep block degenerates into
|
|
67
|
+
* "the first 50 lines" and re-surfaces nothing from the middle. That is the honest
|
|
68
|
+
* limit of a red-set that cannot know the stage's own pattern (see
|
|
69
|
+
* `REPLAN_GAP_KEEP`), and it is affordable precisely because it now applies only
|
|
70
|
+
* to a gap nothing upstream ever bounded — on that gap ANY envelope loses names,
|
|
71
|
+
* and the choice is a bounded prompt over an unbounded one.
|
|
72
|
+
*
|
|
73
|
+
* No gap → the empty string, so the brief renders byte-identically to the pre-F86
|
|
74
|
+
* one. A labelled empty section would be an invitation to explain an absence the
|
|
75
|
+
* run never observed (a stall never judged its exits at all).
|
|
76
|
+
*
|
|
77
|
+
* @param {string | null | undefined} gap the step's last exit gap text
|
|
78
|
+
* @returns {string} the labelled block, or '' when there is nothing to show
|
|
17
79
|
*/
|
|
18
|
-
export function
|
|
80
|
+
export function closeGapBlock(gap: string | null | undefined): string;
|
|
19
81
|
/**
|
|
20
82
|
* The plan-drafting prompt: a schema DESCRIPTION built from the live validator
|
|
21
83
|
* menus — never a copyable example (the drafter must author, not echo; the
|
|
@@ -147,6 +209,40 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
147
209
|
* `flat` — "revise the goal" — on a run that was converging when its allowance ran
|
|
148
210
|
* out. Baselines only (src/trend.js's THE SEED): it spends none of this leg's own
|
|
149
211
|
* bounds and mints none of its strikes. Empty/omitted is the cold path.
|
|
212
|
+
* @param {{count?: number, grantUsed?: boolean}|null} [opts.resumeReplans] RESUME — the
|
|
213
|
+
* replan LEDGER the chain has already spent (`readResume`'s `restart.replans` /
|
|
214
|
+
* `restart.replanGrantUsed`), seeding the ceiling below. `resumeGrades`' sibling and
|
|
215
|
+
* deliberately NOT its twin: a grade seed feeds a READOUT, this one feeds a BOUND.
|
|
216
|
+
*
|
|
217
|
+
* Without it the ceiling is reborn on every call, and a resume is another call:
|
|
218
|
+
* measured, a leg that spent both replans and step-redded handed its checkpoint to a
|
|
219
|
+
* second leg that replanned twice more. Two became four by being killed once, which is
|
|
220
|
+
* the creep the latch exists to make impossible ("unlimited replanning launders thrash
|
|
221
|
+
* as adaptation", PRD v1.12).
|
|
222
|
+
*
|
|
223
|
+
* WHY THIS SEED SPENDS THE LEG'S BOUND WHERE `resumeGrades` DOES NOT. src/trend.js's
|
|
224
|
+
* THE SEED refuses to seed the fix loop's ITERATIONS from history "because the leg's
|
|
225
|
+
* own bounds must not be spent by history" — and that is right, because an attempt
|
|
226
|
+
* allowance is a LEG bound: a restarted leg buys its own attempts with its own money.
|
|
227
|
+
* The replan ceiling is a RUN bound by doctrine. It bounds how many times the WORKFLOW
|
|
228
|
+
* may be redrawn before redrawing it is thrash, and that question spans exactly the
|
|
229
|
+
* chain the halt readout already spans. The two seeds therefore disagree on purpose;
|
|
230
|
+
* they are not the same rule applied inconsistently.
|
|
231
|
+
*
|
|
232
|
+
* CHAIN SEMANTICS, and the one place this differs from the grade seed's mechanism.
|
|
233
|
+
* `readGradeSeed`'s documented KNOWN LIMIT is that it reads ONE spine, so a resume of a
|
|
234
|
+
* resume inherits leg 2's grades and not leg 1's — the chain shortens by one leg per
|
|
235
|
+
* resume, which is fail-safe for a READOUT (a shorter chain can only under-claim a
|
|
236
|
+
* direction). It is the DANGEROUS direction for a ceiling: an under-claimed ledger is a
|
|
237
|
+
* refilled allowance, and every kill would buy one. So this seed does NOT reuse that
|
|
238
|
+
* mechanism. It follows the MONEY fold instead (`priorSpentUsd`, `try-start`/
|
|
239
|
+
* `job-start`): each leg DECLARES the ledger it inherited on its own spine, and the
|
|
240
|
+
* next reader adds only that leg's own `replan` records to the declared number. Leg 3
|
|
241
|
+
* therefore inherits the whole chain, not leg 2's slice.
|
|
242
|
+
*
|
|
243
|
+
* `count` seeds `replans` (and, above zero, latches `replanned`); `grantUsed` seeds the
|
|
244
|
+
* F85-C variance latch, so a killed leg cannot re-earn an extra the arbiter has already
|
|
245
|
+
* granted. Null/omitted is the cold path and is byte-identical to a run nobody resumed.
|
|
150
246
|
* @param {() => boolean} [opts.spendComplete] is the run's spend EXACT, or a floor? The
|
|
151
247
|
* ledger one level up owns the answer (unpriced rounds, a self-healed stall, a mid-call
|
|
152
248
|
* cut, an inherited resume floor); the money-halt readout states it beside the remaining
|
|
@@ -161,7 +257,7 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
161
257
|
* 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
|
|
162
258
|
* `step-red:<id>`
|
|
163
259
|
*/
|
|
164
|
-
export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades }: {
|
|
260
|
+
export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
|
|
165
261
|
workdir: string;
|
|
166
262
|
provider: any;
|
|
167
263
|
nativeProvider?: ((o: {
|
|
@@ -187,6 +283,10 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
|
|
|
187
283
|
stage?: string | null;
|
|
188
284
|
value?: number | null;
|
|
189
285
|
}[] | undefined;
|
|
286
|
+
resumeReplans?: {
|
|
287
|
+
count?: number;
|
|
288
|
+
grantUsed?: boolean;
|
|
289
|
+
} | null | undefined;
|
|
190
290
|
spendComplete?: (() => boolean) | undefined;
|
|
191
291
|
now?: (() => number) | undefined;
|
|
192
292
|
}): Promise<string>;
|
package/types/ralph.d.ts
CHANGED
|
@@ -111,6 +111,22 @@ export function runStages(stages: any[], redact?: (s: string) => string, { timeo
|
|
|
111
111
|
timeoutMs?: number;
|
|
112
112
|
cwd?: string;
|
|
113
113
|
}): Promise<any>;
|
|
114
|
+
/**
|
|
115
|
+
* Bound an over-long close stream to a head sample + elided middle + tail. When
|
|
116
|
+
* `keepPattern` is set (F28), lines of the WHOLE stream matching it are also
|
|
117
|
+
* carried in a clearly-delimited block between head and tail — capped — so the
|
|
118
|
+
* failing-test names buried in the middle still reach the worker.
|
|
119
|
+
* Exported for the ONE reason `GAP_KEEP_TRIM_MARKER` above is: a second consumer
|
|
120
|
+
* that needs a gap bounded must reuse THIS envelope, not spell its own. A private
|
|
121
|
+
* copy would be a second truncation scheme, and the two would drift on exactly the
|
|
122
|
+
* property that matters — whether the red lines survive, and whether the trim
|
|
123
|
+
* announces itself (F28: silent truncation is the disease this function cures).
|
|
124
|
+
* The close path is unchanged: `runClose` still calls it with the stage's own
|
|
125
|
+
* `gapKeep`, byte-identically.
|
|
126
|
+
* @param {string} s the (redacted) close output
|
|
127
|
+
* @param {string} [keepPattern] regex source; matching lines survive the elision
|
|
128
|
+
*/
|
|
129
|
+
export function boundGap(s: string, keepPattern?: string): string;
|
|
114
130
|
/**
|
|
115
131
|
* The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
|
|
116
132
|
* happens to return". Two governors implement it and they read different signals —
|