bareloop 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +204 -0
- package/README.md +101 -117
- package/bareloop.context.md +139 -12
- package/package.json +1 -1
- package/src/authorflow.js +189 -22
- package/src/authoring.js +190 -0
- package/src/authorjob.js +11 -3
- package/src/authorscout.js +70 -9
- package/src/bridges.js +14 -1
- package/src/index.js +3 -0
- package/src/kinds.js +102 -11
- package/src/ledger.js +5 -1
- package/src/planrun.js +210 -25
- package/src/ralph.js +5 -0
- package/src/reuse.js +52 -2
- package/src/run.js +2 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +52 -8
- package/types/authoring.d.ts +112 -0
- package/types/authorjob.d.ts +3 -2
- package/types/authorscout.d.ts +16 -3
- package/types/index.d.ts +1 -1
- package/types/kinds.d.ts +58 -6
- package/types/planrun.d.ts +43 -0
- package/types/reuse.d.ts +1 -1
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/src/planrun.js
CHANGED
|
@@ -34,7 +34,7 @@ import { validateBridge, loadGate } from './bridges.js';
|
|
|
34
34
|
import { extractArtifact } from './text.js';
|
|
35
35
|
import { createClock, isWallTimeout } from './clock.js';
|
|
36
36
|
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade } from './declaredclose.js';
|
|
37
|
-
import { seedAtHead, seedListing } from './kinds.js';
|
|
37
|
+
import { seedAtHead, seedListing, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
|
|
38
38
|
import { workBranchName, prepareWorkBranch } from './workbranch.js';
|
|
39
39
|
|
|
40
40
|
const require = createRequire(import.meta.url);
|
|
@@ -260,6 +260,60 @@ export function closeGapBlock(gap) {
|
|
|
260
260
|
+ (scrubbed.length > CHECK_GAP_MAX ? boundGap(scrubbed, REPLAN_GAP_KEEP) : scrubbed);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The recorded bound-reason's ceiling in the WORKER-facing note. The reason is a
|
|
265
|
+
* provider string (`denied:<tool>` today, ~20 bytes), not a gap, so this is a
|
|
266
|
+
* backstop against a future terminal that arrives long — never an envelope the
|
|
267
|
+
* shipped path routinely rides through.
|
|
268
|
+
*/
|
|
269
|
+
export const BOUND_REASON_MAX = 200;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What the NEXT attempt is told about the bound that cut the previous one.
|
|
273
|
+
*
|
|
274
|
+
* Two different bounds used to set the same bare `attemptBounded = roundIteration`
|
|
275
|
+
* and therefore render the same sentence. For a round-bound cutoff that sentence is
|
|
276
|
+
* true. For BA-11's deny streak it is false twice over — the rounds did NOT run out
|
|
277
|
+
* (the fence short-circuited the attempt with budget left), and the count it quotes
|
|
278
|
+
* is the cap, not what was spent — and it aims the worker at the READ BUDGET when the
|
|
279
|
+
* thing that stopped it was the FENCE. The reason was already recorded on the spine;
|
|
280
|
+
* nothing carried it to the one reader who could act on it.
|
|
281
|
+
*
|
|
282
|
+
* The denial note is MECHANICAL in the strict sense: it quotes the terminal the
|
|
283
|
+
* provider actually returned and names which bound fired. It invents nothing —
|
|
284
|
+
* no streak count and no denied path, because bare-agent's return (`error:
|
|
285
|
+
* 'denied:<tool>'`, loop.js) carries neither, and a number nobody measured is a
|
|
286
|
+
* bug with a confident voice. Every other cause keeps the frozen wording byte for
|
|
287
|
+
* byte: a wall-bounded attempt reads as a round bound exactly as it did before
|
|
288
|
+
* (and is unreachable anyway — the step head refuses to fund an attempt past the
|
|
289
|
+
* deadline, W-2), so this change can only ever speak where it has something true
|
|
290
|
+
* to add.
|
|
291
|
+
*
|
|
292
|
+
* The reason arrives ALREADY SCRUBBED (the capture seam in `mkWorker` runs it
|
|
293
|
+
* through the one `SECRET_PATTERNS` inventory), so this renderer only bounds it —
|
|
294
|
+
* and announces the trim with the repo's ONE marker rather than a second spelling
|
|
295
|
+
* of "this text was cut" (F90.2).
|
|
296
|
+
*
|
|
297
|
+
* @param {{iteration?: number|string|undefined, cause: string, reason: string|null}|null|undefined} bounded
|
|
298
|
+
* the previous attempt's bound state, or nullish when it was not bounded
|
|
299
|
+
* @param {number} rounds the step's round bound, as quoted by the frozen sentence
|
|
300
|
+
* @returns {string|null} the note, or null when there is nothing to say
|
|
301
|
+
*/
|
|
302
|
+
export function boundedNote(bounded, rounds) {
|
|
303
|
+
if (!bounded) return null;
|
|
304
|
+
if (bounded.cause !== 'denied') {
|
|
305
|
+
return `Your previous attempt was CUT OFF after ${rounds} tool rounds. `
|
|
306
|
+
+ 'Reading is bounded; writing is not. Form a hypothesis EARLY and make the change.';
|
|
307
|
+
}
|
|
308
|
+
const raw = String(bounded.reason ?? '');
|
|
309
|
+
const shown = raw.length > BOUND_REASON_MAX
|
|
310
|
+
? `${raw.slice(0, BOUND_REASON_MAX)} [${GAP_TRIM_MARKER} ${raw.length - BOUND_REASON_MAX} of ${raw.length} characters withheld — the cap is ${BOUND_REASON_MAX}]`
|
|
311
|
+
: raw;
|
|
312
|
+
return 'Your previous attempt was CUT OFF by the gate: it denied consecutive tool calls '
|
|
313
|
+
+ `until the streak guard ended the attempt. The gate recorded: ${shown}. `
|
|
314
|
+
+ 'The round budget was not what stopped you — repeating a denied call ends this attempt the same way.';
|
|
315
|
+
}
|
|
316
|
+
|
|
263
317
|
/**
|
|
264
318
|
* The plan-drafting prompt: a schema DESCRIPTION built from the live validator
|
|
265
319
|
* menus — never a copyable example (the drafter must author, not echo; the
|
|
@@ -321,7 +375,7 @@ Shape: { "schema": "plan-v1", "steps": [ ... 1..${MAX_PLAN_STEPS} steps ... ] }
|
|
|
321
375
|
strictly in array order. Each step (no other fields exist):
|
|
322
376
|
- "id": kebab-case slug, unique
|
|
323
377
|
- "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)
|
|
378
|
+
- "tools": non-empty unique subset of ${JSON.stringify(ceiling)} (read/grep are the worker's ONLY way to see the tree — there is no shell; write/edit change the tree; recall/get/impact/related/recent search and navigate the repository index; compress/peek read cheaply; stash/remember/forget park and record notes across steps)
|
|
325
379
|
- "rounds": integer 1..${maxStepRounds} — the step's per-attempt tool-round bound
|
|
326
380
|
- "target": the step's deliverable path (REQUIRED when tools include write/edit), inside ${JSON.stringify(job.writeScope)}
|
|
327
381
|
- "scope" (optional): narrow this step's WRITE fence — copy one value from the offered scopes below.
|
|
@@ -351,6 +405,15 @@ strictly in array order. Each step (no other fields exist):
|
|
|
351
405
|
exit reds on that file can never finish, on any attempt.${seedRedLaw}${priorChecksLaw}
|
|
352
406
|
Reference checks by NAME only; you cannot author or modify one.
|
|
353
407
|
|
|
408
|
+
The worker has NO SHELL. It cannot run a command, a compiler, a linter, or a test suite,
|
|
409
|
+
and it cannot execute a script it writes: its only verbs are the tools you grant that step
|
|
410
|
+
in "tools". The check named in a "check-passes" exit is the ONLY execution in this run —
|
|
411
|
+
the shell runs it after each attempt and hands the worker its raw output. So write every
|
|
412
|
+
"action" for a worker that locates its work by READING and GREPPING files and by reading
|
|
413
|
+
the check output it is given. Never tell it to run, execute, re-run, or verify by running
|
|
414
|
+
anything (an action that opens with "Run \`<command>\`…" costs the whole step: the worker
|
|
415
|
+
has no way to obey it and spends its rounds hunting by hand).
|
|
416
|
+
|
|
354
417
|
Offered "scope" values for tree-changed — copy ONE of these exactly, character for
|
|
355
418
|
character. No other value is accepted, and patterns of your own (like "src/*.js")
|
|
356
419
|
are not accepted:
|
|
@@ -1062,7 +1125,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1062
1125
|
const lc = new LiteCtx({ root: workdir });
|
|
1063
1126
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
1064
1127
|
const fencePrefixes = job.writeScope.map((/** @type {string} */ g) => resolve(workdir, globToPrefix(g)));
|
|
1065
|
-
const auditPath = join(workdir,
|
|
1128
|
+
const auditPath = join(workdir, GATE_AUDIT_FILE);
|
|
1066
1129
|
const chainByName = new Map(menu.map((m) => [m.name, m.run]));
|
|
1067
1130
|
// Choose-don't-describe (§4): the offered `tree-changed` scopes, enumerated from
|
|
1068
1131
|
// the signed fence plus the directories that actually exist beneath it. ONE menu
|
|
@@ -1198,7 +1261,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1198
1261
|
// uses), so this can only tighten, never widen
|
|
1199
1262
|
writeScope: writable ? (fence ?? fencePrefixes) : [],
|
|
1200
1263
|
readScope: [workdir],
|
|
1201
|
-
deny: [auditPath,
|
|
1264
|
+
deny: [auditPath, ...ARBITER_BOOK_STORES.map((s) => join(workdir, s))],
|
|
1202
1265
|
},
|
|
1203
1266
|
budget: { maxCostUsd: Math.max(remainingUsd(), 0.0001) },
|
|
1204
1267
|
limits: { maxTurns },
|
|
@@ -1312,7 +1375,23 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1312
1375
|
/** @type {number|string|undefined} */
|
|
1313
1376
|
let roundIteration;
|
|
1314
1377
|
let roundsThisAttempt = 0;
|
|
1315
|
-
/**
|
|
1378
|
+
/**
|
|
1379
|
+
* WHICH bound cut the last attempt, and — when the provider recorded one — its
|
|
1380
|
+
* reason. A bare iteration number was the whole state here, so three different
|
|
1381
|
+
* bounds (round cap, wall, BA-11 deny streak) were indistinguishable to the one
|
|
1382
|
+
* consumer that speaks to the worker, which then told every one of them the
|
|
1383
|
+
* round cap had run out. The cause travels WITH the iteration because they are
|
|
1384
|
+
* read together and can never be allowed to drift apart.
|
|
1385
|
+
*
|
|
1386
|
+
* `reason` is scrubbed at THIS seam — the boundary, once, before any reader —
|
|
1387
|
+
* so no consumer has to remember to do it (the same placement the exit results
|
|
1388
|
+
* get in `judge`). It rides the spine event in the same scrubbed spelling: one
|
|
1389
|
+
* value, one inventory, and an append-only log is forever.
|
|
1390
|
+
* `iteration` mirrors `roundIteration` exactly, undefined included: the bound
|
|
1391
|
+
* cannot know a label the run never set, and inventing one here would be the
|
|
1392
|
+
* only place in this file that claims to.
|
|
1393
|
+
* @type {{iteration: number|string|undefined, cause: 'rounds'|'wall'|'denied', reason: string|null}|undefined}
|
|
1394
|
+
*/
|
|
1316
1395
|
let attemptBounded;
|
|
1317
1396
|
/**
|
|
1318
1397
|
* Open an iteration: label the rounds, reset the per-iteration round counter,
|
|
@@ -1413,19 +1492,24 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1413
1492
|
}
|
|
1414
1493
|
// a maxTurns session is a BOUNDED attempt, not an escalation — the same
|
|
1415
1494
|
// 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
|
-
}
|
|
1495
|
+
// feed the gap forward (the CLI preserves lastText, BA-5). A DENIAL
|
|
1496
|
+
// STREAK is the same shape and takes the same lane (see the Loop path's
|
|
1497
|
+
// note below — one doctrine, both surfaces).
|
|
1422
1498
|
if (r.error) {
|
|
1499
|
+
if (r.error === 'max_turns' || r.error.startsWith('denied:')) {
|
|
1500
|
+
// the CAUSE is kept, not just the fact: `max_turns` really is the round
|
|
1501
|
+
// cap, a `denied:` terminal is the fence, and the next attempt is told
|
|
1502
|
+
// which one it was (`boundedNote`)
|
|
1503
|
+
const denied = r.error.startsWith('denied:');
|
|
1504
|
+
const reason = scrub(r.error);
|
|
1505
|
+
attemptBounded = { iteration: roundIteration, cause: denied ? 'denied' : 'rounds', reason };
|
|
1506
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, cap: attemptRounds, native: true, reason });
|
|
1507
|
+
return r;
|
|
1508
|
+
}
|
|
1423
1509
|
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';
|
|
1510
|
+
// halt → cap-halt; a bridge/session terminal (bridge-failed,
|
|
1511
|
+
// session_timeout, session:*) is provider-owned transport
|
|
1512
|
+
err.category = r.error.startsWith('halt:') ? 'cap-halt' : 'provider-red';
|
|
1429
1513
|
err.lib = 'bare-agent';
|
|
1430
1514
|
throw err;
|
|
1431
1515
|
}
|
|
@@ -1523,11 +1607,11 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1523
1607
|
// (a step that would BEGIN past the deadline is never funded), and the head of
|
|
1524
1608
|
// a close-fix iteration (W-2 — the run stops on the verdict already minted).
|
|
1525
1609
|
if (clock.expired()) {
|
|
1526
|
-
attemptBounded = roundIteration;
|
|
1610
|
+
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
1527
1611
|
emit('wall-bounded', { phase, iteration: roundIteration, ...clock.report(closeTimeoutForReport) });
|
|
1528
1612
|
self.stop();
|
|
1529
1613
|
} else if (roundsThisAttempt >= attemptRounds) {
|
|
1530
|
-
attemptBounded = roundIteration;
|
|
1614
|
+
attemptBounded = { iteration: roundIteration, cause: 'rounds', reason: null };
|
|
1531
1615
|
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds });
|
|
1532
1616
|
self.stop();
|
|
1533
1617
|
}
|
|
@@ -1561,12 +1645,25 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1561
1645
|
} catch (e) {
|
|
1562
1646
|
throw categorize(e).err;
|
|
1563
1647
|
}
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1648
|
+
// BA-11's deny streak is a BOUNDED ATTEMPT, not a terminal. It is the same
|
|
1649
|
+
// shape `loop.stop()` has at the round bound thirty lines up: the attempt
|
|
1650
|
+
// ran, produced real work, and was cut short by a governance bound doing
|
|
1651
|
+
// exactly its job — the fence HELD, which is the opposite of a fault. So it
|
|
1652
|
+
// takes the same lane: judge the partial work, feed the gap forward, the
|
|
1653
|
+
// loop continues under unchanged caps. Routing it as a terminal killed a
|
|
1654
|
+
// converging run with money and wall left (u-msn227nq). The native surface
|
|
1655
|
+
// takes the identical lane above — one doctrine, both surfaces.
|
|
1656
|
+
if (r.error && r.error.startsWith('denied:')) {
|
|
1657
|
+
const reason = scrub(r.error);
|
|
1658
|
+
attemptBounded = { iteration: roundIteration, cause: 'denied', reason };
|
|
1659
|
+
emit('attempt-bounded', { phase, iteration: roundIteration, rounds: roundsThisAttempt, cap: attemptRounds, reason });
|
|
1660
|
+
return r;
|
|
1661
|
+
}
|
|
1662
|
+
// the remaining error-return taxonomy (one map, same doctrine as native's):
|
|
1663
|
+
// halt → cap-halt, API truncation → provider-red, anything else is wiring
|
|
1566
1664
|
if (r.error) {
|
|
1567
1665
|
const err = /** @type {CategorizedError} */ (new Error(`worker loop: ${r.error}`));
|
|
1568
1666
|
err.category = r.error.startsWith('halt:') ? 'cap-halt'
|
|
1569
|
-
: r.error.startsWith('denied:') ? 'gate-red'
|
|
1570
1667
|
: r.error.startsWith('truncated:') ? 'provider-red'
|
|
1571
1668
|
: 'interpreter-red';
|
|
1572
1669
|
err.lib = 'bare-agent';
|
|
@@ -2044,14 +2141,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2044
2141
|
// carries counts and paths only — never content (the spine is forever).
|
|
2045
2142
|
const rootInj = root ? root.observe({ iteration, gap, writes: w.workerWrites() }) : null;
|
|
2046
2143
|
if (rootInj) emit('root-injected', { step: step.id, ...rootInj.event });
|
|
2144
|
+
// read ONCE: the note and the "was it the attempt just before this one" test
|
|
2145
|
+
// are two questions about the same record, and two reads is how they drift
|
|
2146
|
+
const bound = w.wasBounded();
|
|
2047
2147
|
const r = await w.ask([
|
|
2048
2148
|
step.action,
|
|
2049
2149
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root — a relative path resolves against a different directory and will be denied by the gate.`,
|
|
2050
2150
|
step.target && `Write your deliverable to: ${resolve(workdir, step.target)}`,
|
|
2051
2151
|
artifacts.length > 0 && `Working context (read-only) — prior steps' results:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
|
|
2052
2152
|
gap && `Previous attempt failed this step's checks:\n${gap}`,
|
|
2053
|
-
|
|
2054
|
-
|
|
2153
|
+
// the note names the bound that ACTUALLY fired — a deny streak and a spent
|
|
2154
|
+
// round cap are different diagnoses with different remedies, and telling a
|
|
2155
|
+
// fenced worker to "form a hypothesis early" aims it at the wrong wall
|
|
2156
|
+
bound?.iteration === iteration - 1 && boundedNote(bound, step.rounds),
|
|
2055
2157
|
rootInj && rootInj.note,
|
|
2056
2158
|
].filter(Boolean).join('\n\n'));
|
|
2057
2159
|
lastText = scrub(r.text ?? '').slice(0, ARTIFACT_MAX);
|
|
@@ -2234,7 +2336,32 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2234
2336
|
&& cat === 'step-variance'
|
|
2235
2337
|
&& !varianceGrantUsed
|
|
2236
2338
|
&& stopTrend.trend === 'converging';
|
|
2237
|
-
|
|
2339
|
+
// W-2 at the replan GATE, hamr's ruling: *"past the wall no new fix cycle or step
|
|
2340
|
+
// starts"* — and a replan is the first move of starting a new step. Past the
|
|
2341
|
+
// deadline the whole cycle is doomed by construction and it is not free: the
|
|
2342
|
+
// drafting call is bought (money the operator's time allowance no longer covers),
|
|
2343
|
+
// the redrafted plan's first step is then refused at its own head by the W-2 check
|
|
2344
|
+
// one level down, and the run wall-halts anyway. u-msmt91t3 paid for exactly that
|
|
2345
|
+
// and called the result `step-red`. So the gate declines to FUND it.
|
|
2346
|
+
//
|
|
2347
|
+
// Nothing is routed here and no readout is duplicated: declining simply lets the
|
|
2348
|
+
// stop fall through to the `step-variance` terminal below, which is the one site
|
|
2349
|
+
// that mints the wall-halt package (record + F11-consistent escalation + levers).
|
|
2350
|
+
// One site, one spelling — a second copy here is how two instruments come to
|
|
2351
|
+
// disagree about one event.
|
|
2352
|
+
//
|
|
2353
|
+
// TRIGGER-AGNOSTIC (hamr: "extend"). It first covered the variance trigger only,
|
|
2354
|
+
// the one measured on u-msmt91t3; the ladder's exhaustion and the stall fuse reach
|
|
2355
|
+
// this gate past the deadline too, and a doomed cycle costs the same money whichever
|
|
2356
|
+
// instrument named the stop. Each trigger then lands on its own honest terminal
|
|
2357
|
+
// below — the wall for the two that would otherwise mint `step-red`, its own name
|
|
2358
|
+
// for the stall (which already rides out as `step-stalled` and must, because run.js
|
|
2359
|
+
// keys the F44 spend FLOOR on that outcome).
|
|
2360
|
+
//
|
|
2361
|
+
// The latch is NOT consumed — no grant was spent, because no replan was bought.
|
|
2362
|
+
// With time on the clock this is byte-identical to today, for every trigger.
|
|
2363
|
+
const wallStopsReplan = clock.expired();
|
|
2364
|
+
if (!wallStopsReplan && (!replanned || grantExtraReplan) && replanTrigger && remainingUsd() > MONEY_MIN) {
|
|
2238
2365
|
if (grantExtraReplan) varianceGrantUsed = true;
|
|
2239
2366
|
replanned = true;
|
|
2240
2367
|
replans += 1;
|
|
@@ -2333,6 +2460,38 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2333
2460
|
skipCount = 0;
|
|
2334
2461
|
continue;
|
|
2335
2462
|
}
|
|
2463
|
+
/**
|
|
2464
|
+
* W-2's STEP-LOOP terminal, and the ONE place it is spelled. Reached whenever a
|
|
2465
|
+
* trigger that would otherwise mint `step-red:<id>` lands with the deadline already
|
|
2466
|
+
* passed: the meter's stop (F85's variance) and the ladder's (attempt exhaustion
|
|
2467
|
+
* with money still on the table). Both are the same event wearing two instruments'
|
|
2468
|
+
* names — the clock is what ended the run — and `step-red` is a CAPABILITY label,
|
|
2469
|
+
* so minting it there files a governance stop as "the work failed". u-msmt91t3 is
|
|
2470
|
+
* the measured instance: `step-red` 9.9 seconds after the run's own `wall-bounded`.
|
|
2471
|
+
*
|
|
2472
|
+
* One site, one spelling, because the record, the F11-consistent escalation and the
|
|
2473
|
+
* lever list must be identical wherever the clock stops a step — a second copy per
|
|
2474
|
+
* trigger is how two instruments come to disagree about one event. The instrument
|
|
2475
|
+
* that named the stop FIRST is not discarded: it rides in the detail, so the human
|
|
2476
|
+
* reads why the step was stopped as well as which lever fits.
|
|
2477
|
+
*
|
|
2478
|
+
* `stepWallStop` is never consulted here by construction: that record is written
|
|
2479
|
+
* only by the head-of-attempt check, and both triggers above are read BEFORE it, so
|
|
2480
|
+
* a step either of them stopped never reached it.
|
|
2481
|
+
* @param {string} stoppedBy the instrument that named the stop first
|
|
2482
|
+
*/
|
|
2483
|
+
const wallHaltTerminal = (stoppedBy) => {
|
|
2484
|
+
emitWallHalt({ cutMidCall: false, phase: `step:${step.id}`, stepsDone: idx, stepsPlanned: plan.steps.length });
|
|
2485
|
+
emit('escalation', {
|
|
2486
|
+
category: 'wall-halt', decisionReady: true, phase: `step:${step.id}`,
|
|
2487
|
+
decision: `The run reached its wall-clock cap while step "${step.id}" was running. Time ran out, not capability — the verdict the run already has stands.`,
|
|
2488
|
+
options: WALL_OPTIONS,
|
|
2489
|
+
detail: `requested ${clock.requestedMs}ms, elapsed ${clock.elapsedMs()}ms. `
|
|
2490
|
+
+ `${stoppedBy}: ${lastEscalation?.detail ?? '(none)'} `
|
|
2491
|
+
+ `Progress trend: ${stopTrend.trend} — ${stopTrend.reading}; ${stopTrend.lever}.`,
|
|
2492
|
+
});
|
|
2493
|
+
return 'wall-halt';
|
|
2494
|
+
};
|
|
2336
2495
|
// F64 — the wall stopped this attempt from INSIDE a provider call (the derived
|
|
2337
2496
|
// call timeout is the deadline in the provider's own coin). ralph already
|
|
2338
2497
|
// emitted the escalation under this category, so the outcome and the record
|
|
@@ -2355,7 +2514,19 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2355
2514
|
// stays a step-red (the stop is a result).
|
|
2356
2515
|
if (cat === 'cap-halt') {
|
|
2357
2516
|
planExecuted();
|
|
2358
|
-
if (remainingUsd() > MONEY_MIN)
|
|
2517
|
+
if (remainingUsd() > MONEY_MIN) {
|
|
2518
|
+
// W-2 EXTENDED to the ladder's stop (hamr: "extend"). This arm is the sibling of
|
|
2519
|
+
// the variance terminal below and carries the identical defect: with money still
|
|
2520
|
+
// on the table it minted `step-red` whatever the clock said, so a step the
|
|
2521
|
+
// deadline ended was filed as a capability failure. The wallet is re-read (it
|
|
2522
|
+
// always was); the clock is re-read too, and it is read FIRST inside this arm
|
|
2523
|
+
// only — a DRAINED wallet keeps its own `cap-halt` terminal below, because money
|
|
2524
|
+
// is the allowance actually blocking that run and its levers are the ones the
|
|
2525
|
+
// human needs (a wall-halt readout there would offer "raise maxWallMs and rerun"
|
|
2526
|
+
// into an immediate money cut).
|
|
2527
|
+
if (clock.expired()) return wallHaltTerminal('The ladder ended the step first');
|
|
2528
|
+
return `step-red:${step.id}`;
|
|
2529
|
+
}
|
|
2359
2530
|
// v1.46 §2: the money cut is decision-ready HERE too, and for the same reason
|
|
2360
2531
|
// W-2 gave the wall a record at every site it stops a run — a category that
|
|
2361
2532
|
// hands the human a readout at one seam and silence at another is the readout
|
|
@@ -2371,8 +2542,22 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2371
2542
|
// upward would mint an outcome run.js and the ledger's class table do not know,
|
|
2372
2543
|
// and an unmapped category is counted as a library bug (ledger.js) when this is a
|
|
2373
2544
|
// planning story. The escalation ralph already emitted carries the real detail.
|
|
2545
|
+
//
|
|
2546
|
+
// W-2 at the meter's own terminal. A `step-variance` stop that lands PAST the
|
|
2547
|
+
// deadline is a TIME stop wearing the meter's name — the clock is what ended the
|
|
2548
|
+
// run, and hamr's ruling names that stop: *"when time is up, keep the grade we
|
|
2549
|
+
// already have and stop"*. Its cap-halt sibling directly above already re-reads its
|
|
2550
|
+
// own governor (the wallet) before minting a terminal; this branch did not, and on
|
|
2551
|
+
// u-msmt91t3 it minted `step-red` 9.9 seconds AFTER the run's own `wall-bounded`
|
|
2552
|
+
// record — a governance stop filed as a capability read, and (the resume gate's
|
|
2553
|
+
// half of the same defect) a checkpoint filed as an answer.
|
|
2554
|
+
//
|
|
2555
|
+
// The clock is read HERE, with nothing in flight, exactly as the three other
|
|
2556
|
+
// terminal sites read it. With time still on the clock this is byte-identical to
|
|
2557
|
+
// what it was: the step-red is the designed stop, and the stop is the result.
|
|
2374
2558
|
if (cat === 'step-variance') {
|
|
2375
2559
|
planExecuted();
|
|
2560
|
+
if (clock.expired()) return wallHaltTerminal('The meter stopped the step first');
|
|
2376
2561
|
return `step-red:${step.id}`;
|
|
2377
2562
|
}
|
|
2378
2563
|
// Any OTHER terminal escalation category is NOT a capability failure: a
|
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).',
|
package/src/reuse.js
CHANGED
|
@@ -665,7 +665,7 @@ function readGradeSeed(seen) {
|
|
|
665
665
|
* which is later and better evidence than the last event). Defaults to the last
|
|
666
666
|
* event's own timestamp — the last sign of life there is.
|
|
667
667
|
* @returns {any} `{ started, approvalHash, specHash, patient, job, bridgeTries,
|
|
668
|
-
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, greened, completed, tried,
|
|
668
|
+
* perTryBudgetUsd, perTryWallMs, ended, endOutcome, wallDerivedHalt, greened, completed, tried,
|
|
669
669
|
* selectionCostUsd, selectionComplete, strayRounds, r1Missing, restart, deathAtKnown,
|
|
670
670
|
* spentUsd, spendComplete, carrySpentUsd, carrySpendComplete }`
|
|
671
671
|
*/
|
|
@@ -816,7 +816,50 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
816
816
|
* CHECKPOINT and re-entering it is the ruling, not a second payment for an answer
|
|
817
817
|
* already in hand. Empty by default, which is what keeps the reuse loop's own
|
|
818
818
|
* graded-row semantics exactly where they were. */
|
|
819
|
-
const
|
|
819
|
+
const recordedOutcome = open?.jobEnd ? String(open.jobEnd.outcome) : null;
|
|
820
|
+
/** D4a — the MISLABELLED wall stop, derived from the run's own primary records.
|
|
821
|
+
*
|
|
822
|
+
* `step-red` is a verdict-shaped terminal and stays non-resumable: a red is an
|
|
823
|
+
* answer. But on u-msmt91t3 the runner minted one 9.9 seconds AFTER its own
|
|
824
|
+
* `wall-bounded` record — a governance stop wearing a capability label, because the
|
|
825
|
+
* step-variance terminal did not re-read the clock (fixed at the source, src/planrun.js).
|
|
826
|
+
* Spines already written under the defect are not re-runnable and cannot be edited (a
|
|
827
|
+
* spine is append-only forever), so the READER derives the truth the file already
|
|
828
|
+
* holds: a run whose own record says the wall was crossed BEFORE it ended is a wall
|
|
829
|
+
* stop, and W-2's ruling applies — it keeps the grades it minted and pauses.
|
|
830
|
+
*
|
|
831
|
+
* Three constraints, each of them load-bearing:
|
|
832
|
+
* * a DERIVATION, never an override. The evidence is the run's own `wall-bounded`
|
|
833
|
+
* event, emitted by the runner at the round seam; no caller can assert this.
|
|
834
|
+
* * `step-red` ONLY, and only when `wall-halt` is itself resumable for this caller.
|
|
835
|
+
* Every other outcome — green, escalated, provider-red, a step-red with no wall
|
|
836
|
+
* record — is untouched, so "a red is an answer" survives intact.
|
|
837
|
+
* * it touches NO close verdict. `step-red` here is a GOVERNANCE stop label; the
|
|
838
|
+
* grades the close rendered are exactly what they were, and the resume seeds
|
|
839
|
+
* from them.
|
|
840
|
+
* @param {any[]} seen @param {any} jobEnd */
|
|
841
|
+
const wallCrossedBefore = (seen, jobEnd) => {
|
|
842
|
+
// strictly BEFORE the terminal: a record written after the run ended could not have
|
|
843
|
+
// been what stopped it. `indexOf` on the window's own array is the ordering there is
|
|
844
|
+
// (the reader never re-sorts), and a jobEnd somehow absent from it reads nothing.
|
|
845
|
+
const end = seen.indexOf(jobEnd);
|
|
846
|
+
if (end < 0) return false;
|
|
847
|
+
for (let i = 0; i < end; i += 1) {
|
|
848
|
+
const e = seen[i];
|
|
849
|
+
if (!isObj(e) || e.type !== 'wall-bounded' || e.bounded !== true) continue;
|
|
850
|
+
// the clock's OWN two spellings of "the deadline passed", both off `clock.report`.
|
|
851
|
+
// Never a re-derivation from timestamps: a second arithmetic for one question is
|
|
852
|
+
// how two instruments come to disagree.
|
|
853
|
+
if (e.remainingMs === 0) return true;
|
|
854
|
+
if (typeof e.elapsedMs === 'number' && typeof e.requestedMs === 'number' && e.elapsedMs >= e.requestedMs) return true;
|
|
855
|
+
}
|
|
856
|
+
return false;
|
|
857
|
+
};
|
|
858
|
+
const wallDerivedHalt = recordedOutcome === 'step-red'
|
|
859
|
+
&& resumableHalt.has('wall-halt')
|
|
860
|
+
&& wallCrossedBefore(open.seen, open.jobEnd);
|
|
861
|
+
const haltedResumable = recordedOutcome !== null
|
|
862
|
+
&& (resumableHalt.has(recordedOutcome) || wallDerivedHalt);
|
|
820
863
|
if (open) {
|
|
821
864
|
if (open.jobEnd && !haltedResumable) {
|
|
822
865
|
// GRADED: runJob returned and the close had its say. Not a restart — the row is
|
|
@@ -963,7 +1006,14 @@ export function readResume(events, { deathAt = null, direct = false, resumableOu
|
|
|
963
1006
|
perTryBudgetUsd: head?.perTryBudgetUsd ?? directHead?.budgetUsd ?? null,
|
|
964
1007
|
perTryWallMs: head?.perTryWallMs ?? null,
|
|
965
1008
|
ended: end !== null || (directEnd !== null && !haltedResumable),
|
|
1009
|
+
// the RECORDED terminal, always — the derivation below never rewrites what the run
|
|
1010
|
+
// said about itself, it only says how this reader read it
|
|
966
1011
|
endOutcome: end?.outcome ?? directEnd?.outcome ?? null,
|
|
1012
|
+
/** D4a — true when a recorded `step-red` was re-read as a wall-halt off this run's
|
|
1013
|
+
* OWN `wall-bounded` record. Surfaced rather than silent: an operator resuming a
|
|
1014
|
+
* run whose spine says `step-red` must be told which record turned it into a
|
|
1015
|
+
* checkpoint, or the gate looks like it simply stopped refusing. */
|
|
1016
|
+
wallDerivedHalt,
|
|
967
1017
|
greened: completed.some((t) => t.verdictClass === 'green'),
|
|
968
1018
|
completed,
|
|
969
1019
|
tried,
|
package/src/run.js
CHANGED
|
@@ -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'));
|
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. '
|