bareloop 0.6.0 → 0.7.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 +407 -0
- package/README.md +28 -11
- package/bareloop.context.md +510 -28
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/index.js +29 -0
- package/src/ladder.js +196 -0
- package/src/ledger.js +27 -0
- package/src/plan.js +75 -7
- package/src/planrun.js +762 -146
- package/src/ralph.js +148 -28
- package/src/reuse.js +1602 -0
- package/src/run.js +73 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/index.d.ts +3 -0
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +18 -3
- package/types/planrun.d.ts +108 -9
- package/types/ralph.d.ts +82 -4
- package/types/reuse.d.ts +388 -0
- package/types/run.d.ts +50 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
package/bareloop.context.md
CHANGED
|
@@ -152,7 +152,7 @@ contract the single close object has always used) plus:
|
|
|
152
152
|
| stage field | shape | notes |
|
|
153
153
|
|---|---|---|
|
|
154
154
|
| `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
|
|
155
|
-
| `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the
|
|
155
|
+
| `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the ten specs in `jobs/`, four hide a final `verdict` stage and six hide a `changed-from-seed` precondition instead (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
|
|
156
156
|
| `needs` | optional non-empty array of EARLIER stage names | a stage that reads what an earlier stage built (e.g. "the public API matches what an earlier stage emitted") names its prerequisite chain; picking it via `check-passes` runs the chain first, then the stage itself. Every name must be declared before this stage (`invalid-value` otherwise); `needs` + `offer: false` together is incoherent (`invalid-value`) — a stage with a chain to run must be reachable |
|
|
157
157
|
|
|
158
158
|
The stages run in declared order as the close itself; the first red renders the verdict and
|
|
@@ -161,6 +161,19 @@ run chain, prerequisites first); a hidden or partial menu is an acceptable case,
|
|
|
161
161
|
failure — `check-menu` on the spine reports `hidden` + `meaning` whenever the derived menu is
|
|
162
162
|
narrower than the stage list.
|
|
163
163
|
|
|
164
|
+
**The law for close authors: ONE POPULATION PER STAGE.** The trend reader buckets one series
|
|
165
|
+
per stage NAME, and a stage name is not an axis — nothing in the library can stop a stage from
|
|
166
|
+
redding on two structurally different measurements under one name, and a run that crosses that
|
|
167
|
+
seam donates both genres to one series (a 29 → 4 across such a seam reads *converging* and
|
|
168
|
+
would recommend a top-up on work that had only swapped which wall it was behind). If a stage
|
|
169
|
+
can report two unlike numbers, **split it into two stages** in the close, each where its branch
|
|
170
|
+
already runs — the gate sequence is unchanged because the close is first-red-wins. The shipped
|
|
171
|
+
`u-*` closes were split exactly that way (`typecheck` / `typecheck-outside` /
|
|
172
|
+
`tests-kept` / `suite-green`), and the fix is never a sharpened reader: `readGrade` is one
|
|
173
|
+
reader for every close, deliberately (the F49 precedent). **Splitting a stage moves the spec
|
|
174
|
+
hash** — stage names are in the signed spec — so it is a spec edit the operator re-signs, and
|
|
175
|
+
it changes what stored bridges match (below).
|
|
176
|
+
|
|
164
177
|
**Close types and the hierarchy — the OBJECT form, which survives only for the
|
|
165
178
|
declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
|
|
166
179
|
is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
|
|
@@ -237,7 +250,7 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
|
|
|
237
250
|
| `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
|
|
238
251
|
| `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
|
|
239
252
|
| `steps[].model` | optional; `sonnet` \| `haiku` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
|
|
240
|
-
| `steps[].attempts` | optional; int
|
|
253
|
+
| `steps[].attempts` | optional; int `>= 1` | **RETIRED and INERT (F79) — it bounds nothing.** A step's iterations are governed by the STRIKE LADDER (below), which is shell-owned and inexpressible from the plan. The field is no longer offered in the drafting prompt and the runner ignores it; it is deliberately still ACCEPTED, because stored bridge plans carry it and a plan minted under one runner's number must not red under another's. The shape is still checked (a known field holding garbage stays a `bounds` red); the old `<= capRuns` ceiling is gone with the cap it named. It was previously TIGHTEN-only — which meant a step that needed MORE iterations could not ask, and drafters measurably tightened it instead (F77) |
|
|
241
254
|
| `steps[].scope` | optional; one value from the SAME menu `tree-changed` uses | narrows this step's live WRITE fence to a subset of the signed `writeScope` — chosen from the offered scope menu (below), never authored. Off-menu reds `invalid-value` carrying the menu |
|
|
242
255
|
| `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. **`tree-changed.scope` is a MENU, not a glob the agent authors** — the runner enumerates the signed fence entries plus the real directories beneath them (shallowest-first, capped at `MAX_SCOPE_MENU`=24; `legalScopes` in `src/plan.js`), `planPrompt` lists them verbatim, and `validatePlan` accepts membership only — the SAME array on both sides, so what was offered is what is accepted; omitted, it derives from `writeScope` alone — never a free-text fallback. An off-menu value that escapes the fence still reds `scope-escape` (the ledger's attribution class), an in-fence unoffered value reds `invalid-value` carrying the menu. `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` so a capped menu is never silently complete. `check-passes` must name a stage on the close's DERIVED menu (`check-unknown` red names the offered menu — no operator authors this list, it comes straight from `checkMenu(close)`); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). **`check-passes` also requires a write-class verb (`write`/`edit`) on the SAME step** (`exit-illegal` — BREAKING for plan authors: a read-only "verify" step carrying a check used to validate and no longer does). A failing check's gap is re-delivered to that step's OWN worker, so a step holding a check must be able to act on it; a read-only one is a mailbox with no hands and stalls to cap on a byte-identical gap (measured on 4 of 4 drafted plans). The check belongs on the step that fixes; the run's final verification is the operator's close, which the agent never authors. The rule is stated identically in the drafter prompt and the validator, so the two can never disagree — and it is suppressed when `tools` failed to parse (the step's hands are unknowable then, and the real defect already redded: one defect, one red). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
|
|
243
256
|
|
|
@@ -273,9 +286,22 @@ Append-only JSONL event emitter bound to one file. `seq` monotonic per spine, `t
|
|
|
273
286
|
last. Consumers are pure listeners; nothing reads the file back. Returns each event as
|
|
274
287
|
written.
|
|
275
288
|
|
|
276
|
-
### `ralph({ middle, close?, judge?, capRuns
|
|
277
|
-
|
|
278
|
-
The dumb outer shell: `while close-red and
|
|
289
|
+
### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
290
|
+
|
|
291
|
+
The dumb outer shell: `while close-red and not exhausted: run the middle`. **Exhaustion has
|
|
292
|
+
two alternative rules and you supply exactly one** — `capRuns` (a fixed number of middle runs)
|
|
293
|
+
or `ladder` (a PROGRESS governor). With a governor the iteration count floats and the loop
|
|
294
|
+
ends on STRIKES instead; ralph still interprets nothing — it hands the governor each red
|
|
295
|
+
iteration's gap and reads back a boolean. Both rules exhaust through the SAME terminal:
|
|
296
|
+
category `cap-halt`, the same three records in the same order, so nothing downstream (the
|
|
297
|
+
ledger's excluded-set, the step loop's one replan trigger) has to know which rule fired.
|
|
298
|
+
`ladder` is an INTERFACE, not one module: `runPlan` wires `createLadder` (`src/ladder.js`,
|
|
299
|
+
repeats + writes) per step and — since v1.46 — the close TREND governor (`src/trend.js`,
|
|
300
|
+
per-stage graded numbers) on the close-fix loop. A governor may supply an optional
|
|
301
|
+
`terminal()` to override the exhaustion PROSE (the step ladder's copy offers a replan and
|
|
302
|
+
there is no planner at the close); it may never override the category or the outcome. Both
|
|
303
|
+
modules are INTERNAL (not on the public surface), so a direct `ralph` caller uses
|
|
304
|
+
`capRuns` — the strike rule reaches you through `runJob`/`runPlan`'s `strikeLimit`. `close` is an argv
|
|
279
305
|
whose exit code is truth (`runClose` is also exported — **async since 0.6: `runClose` and
|
|
280
306
|
`runStages` return Promises**; the child is awaited instead of spawnSync so a running close
|
|
281
307
|
no longer freezes the host event loop (F68); every close semantic — timeout signal, output
|
|
@@ -298,14 +324,17 @@ if it is still there 2s later, ended with SIGKILL: SIGTERM is a request a close
|
|
|
298
324
|
The verdict is untouched — the FIRST fault named the outcome and the kill only enforces it.
|
|
299
325
|
|
|
300
326
|
**The close child's environment is stripped of credential-shaped variables before it runs**
|
|
301
|
-
(`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`,
|
|
302
|
-
`_API_KEY`/`_SECRET`/`_TOKEN`/`_PASSWORD`/`_CREDENTIAL(S)
|
|
327
|
+
(`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`, any name ending
|
|
328
|
+
`_API_KEY`/`_SECRET`/`_TOKEN`/`_PASSWORD`/`_CREDENTIAL(S)`, plus known credential spellings
|
|
329
|
+
the suffix rule cannot see — `PGPASSWORD`, `MYSQL_PWD`, `SSH_PRIVATE_KEY`, bare
|
|
330
|
+
`TOKEN`/`SECRET`/`API_KEY`, …) — a close judges a tree and never
|
|
303
331
|
needs them. A COPY is stripped, never `process.env`, so the host keeps the key it spends every
|
|
304
332
|
round. This is exposure reduction, not a sandbox: worker-authored code run by the close still
|
|
305
333
|
has network and your OS user's permissions. The strip has no opt-out — no plan, config, or
|
|
306
334
|
spec field can widen it — so if your close genuinely needs a credential-shaped variable
|
|
307
335
|
(`AWS_*` regions/profiles included, e.g. a suite booting against localstack with a test DB
|
|
308
|
-
password
|
|
336
|
+
password, or `PGPASSWORD` for one that hits a real postgres), it will not be there and the
|
|
337
|
+
close reds; set such values inside the close command
|
|
309
338
|
itself, not the inherited environment.
|
|
310
339
|
|
|
311
340
|
**The forbidden zone (PRD v1.11 / F17).** `runClose` returns a verdict ONLY when judgment
|
|
@@ -366,7 +395,7 @@ generic unknown-field. Menus exported: `CLOSE_TYPES`, `CLASS_BY_CLOSE`, `GOLD_CO
|
|
|
366
395
|
`CADENCE_UNITS`, `PROVIDERS`, `CONDITION_KEYS`, `TOOL_MENU`, `LOCKED_TOOLS`, `STORE_VERBS`,
|
|
367
396
|
`VERDICT_TYPES`, `LOCKED_VERDICTS` — plus `checkMenu` itself.
|
|
368
397
|
|
|
369
|
-
### `validatePlan(input, { job, maxStepRounds?, scopes
|
|
398
|
+
### `validatePlan(input, { job, maxStepRounds?, scopes? })` → `{ ok, reds, plan }` — `src/plan.js`
|
|
370
399
|
|
|
371
400
|
`validateJob`'s SIBLING in the two-document split, never a third validator over it (the
|
|
372
401
|
two-doc split's third validator never happens: plan-v1 gates the PLAN, the job spec stays
|
|
@@ -376,7 +405,8 @@ from `job` (a missing or non-plan-shape job fails CLOSED, `job-invalid`). Never
|
|
|
376
405
|
same `{ code, path, detail }` red shape as its siblings; `verb-escape` reds carry the
|
|
377
406
|
escaping verb as a structured `verb` field (the ledger keys on it). `maxStepRounds`
|
|
378
407
|
(default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds`;
|
|
379
|
-
`capRuns
|
|
408
|
+
**the `capRuns?` option is GONE** (F79) — it existed only to ceiling `attempts`, now
|
|
409
|
+
retired-but-accepted, so a plan carries no iteration bound to check; `scopes` is the offered
|
|
380
410
|
`tree-changed` scope menu — the array of scope strings the drafter prompt listed, which
|
|
381
411
|
`runJob`/`runPlan` build internally from the signed fence plus the real directories beneath
|
|
382
412
|
it (a direct `validatePlan` caller may pass its own array); omitted, it derives from the
|
|
@@ -424,7 +454,10 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
|
|
|
424
454
|
`coordination-red` — a failure between units (scope contention, step order, store
|
|
425
455
|
races), never to be folded into worker/interpreter reds.
|
|
426
456
|
|
|
427
|
-
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, shellCapUsd?, closeTimeoutMs?, layerRoot? })` → outcome — `src/run.js`
|
|
457
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades? })` → outcome — `src/run.js`
|
|
458
|
+
|
|
459
|
+
The last four are the RESUME fold and are documented under *Resuming a killed run* below;
|
|
460
|
+
they default to `0` / `true` / `0` / `null`, so a fresh run passes none of them.
|
|
428
461
|
|
|
429
462
|
The runner — the shell's top layer, and the ONE entry. It composes everything below it and
|
|
430
463
|
interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
|
|
@@ -434,19 +467,24 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
|
|
|
434
467
|
`min(job budget − spent, shell cap)`.
|
|
435
468
|
|
|
436
469
|
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
437
|
-
plan-red | check-red | close-red | close-unsupported |
|
|
438
|
-
interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`. `provider-red` is a
|
|
470
|
+
plan-red | check-red | close-red | close-unsupported | recipe-stale | pricing-red |
|
|
471
|
+
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`. `provider-red` is a
|
|
439
472
|
transport throw or a worker round the API cut off mid-generation (`truncated:max_tokens`,
|
|
440
473
|
BA-6 — before which it laundered into a clean finish, F25): no verdict exists and the failed
|
|
441
474
|
round's spend is only partly known (F6). `cap-halt` is the wallet; `wall-halt` is the clock
|
|
442
475
|
(F64 — a timeout derived from the run's own deadline is a governance stop, never a transport
|
|
443
476
|
casualty). **`cap-halt` reaches you from the close-fix loop too, not only from a step:** the
|
|
444
|
-
shell spells
|
|
477
|
+
shell spells exhaustion (strikes on a step, and — since v1.46 — strikes in the fix loop as
|
|
478
|
+
well, `capRuns` only while that loop's trend instrument is blind) and a money-gate halt with
|
|
479
|
+
the same category, so both the
|
|
445
480
|
step loop and the fix loop read the WALLET to tell them apart — a drained wallet is
|
|
446
|
-
`cap-halt` (the resume-to-cap checkpoint),
|
|
481
|
+
`cap-halt` (the resume-to-cap checkpoint), exhaustion with money still on the table is
|
|
447
482
|
the designed `escalated` terminal ("the close is still red"). The fix worker's gate is built
|
|
448
483
|
with the wallet at its most drained, which is exactly where a money cut would otherwise
|
|
449
|
-
masquerade as a capability read (F45). `
|
|
484
|
+
masquerade as a capability read (F45). A `cap-halt` from the wallet also emits the
|
|
485
|
+
decision-ready `money-halt` record described under the plan flow below — the kept verdict, the
|
|
486
|
+
trend and the three levers — so a consumer never has to reconstruct the stop from the outcome
|
|
487
|
+
name alone. `step-stalled` is the F66 stall fuse giving up: no
|
|
450
488
|
completed round for 5 minutes,
|
|
451
489
|
three times on one call — each stall silently abandons the hung call and reissues it
|
|
452
490
|
(self-heal first); only the third throws. Inside a step it is the THIRD replan trigger
|
|
@@ -462,11 +500,13 @@ decision-ready escalation with a terminal `job-end`: the spine never dangles.
|
|
|
462
500
|
of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
|
|
463
501
|
and it is stated even on the pre-token reds, where the honest figure is a real `0`.
|
|
464
502
|
`spendComplete` says whether that figure is EXACT; `false` means `spentUsd` is a FLOOR ("at
|
|
465
|
-
least $X", true total unknowable) and must not be read as a total.
|
|
503
|
+
least $X", true total unknowable) and must not be read as a total. Four things set it, all
|
|
466
504
|
the same class — a call whose cost is unknowable from here: a round that came back unpriced
|
|
467
505
|
(F6); a run that ABSORBED a stall, because the fuse's silent reissue may pay twice for one
|
|
468
|
-
answer and the flag is one-way whatever the outcome;
|
|
469
|
-
mid-flight (`cutMidCall`)
|
|
506
|
+
answer and the flag is one-way whatever the outcome; a `wall-halt` that cut a call
|
|
507
|
+
mid-flight (`cutMidCall`); and a RESUME whose fold was itself a floor
|
|
508
|
+
(`priorSpendComplete: false` — an unknown does not heal by being carried forward, so every
|
|
509
|
+
round of this attempt being priced repairs nothing about the one before it). A wall stop read BETWEEN iterations or steps has no call in flight,
|
|
470
510
|
so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
|
|
471
511
|
field presence and never has to launder a missing `spentUsd` into `$0`.
|
|
472
512
|
|
|
@@ -481,16 +521,121 @@ write-class nor the store-class verbs are in its menu; hard-bounded rounds) →
|
|
|
481
521
|
the planner never sees the repo, only the scout blob; drafted against a schema
|
|
482
522
|
description with check NAMES only; `validatePlan` gates it, one redraft with the reds
|
|
483
523
|
fed back, then `plan-red`) → **EXECUTE** (strictly sequential micro-loops: `ralph` with
|
|
484
|
-
the exit-evaluator judge
|
|
524
|
+
the exit-evaluator judge, each step under its own STRIKE LADDER — below; tree snapshots at
|
|
525
|
+
step start; the gap names every failing
|
|
485
526
|
wall — mechanical genre, F46's measured mechanism; artifacts feed forward labeled by
|
|
486
527
|
step id) → **ONE replan**, triggered by exhaustion OR variance (an instrument stop never
|
|
487
528
|
replans) → **the operator's close**, a red feeding ONE bounded fix loop judged by the
|
|
488
|
-
REAL close
|
|
529
|
+
REAL close and governed by the close TREND rule below (`capRuns`, default 3, survives only as
|
|
530
|
+
its blind fallback). `plan-executed` (the plan-as-executed record, design law #2) lands on the
|
|
489
531
|
spine on every path that executed steps. Additional outcomes: `already-green |
|
|
490
532
|
plan-red | check-red | close-red | wall-halt`. Worker prompts hold the v1.12 §5 contract
|
|
491
533
|
(mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
|
|
492
534
|
the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
|
|
493
535
|
|
|
536
|
+
**The strike ladder — how a STEP ends (F77–F79).** A plan step is **not** bounded by a
|
|
537
|
+
number of iterations. It runs until it stops making PROGRESS: a *strike* is a red iteration
|
|
538
|
+
that repeats an already-seen gap or wrote nothing, and `strikeLimit` strikes (shell option on
|
|
539
|
+
`runJob`/`runPlan`, **default 2**) end the step. Concretely — repeats are matched against a
|
|
540
|
+
**seen-set** for the whole step, never just the last gap (an A→B→A oscillator is real and
|
|
541
|
+
last-only reads it as progress); the gap is normalized for COMPARISON only (TAP `duration_ms`
|
|
542
|
+
lines, ISO stamps and `ms` figures dropped so a re-run is byte-stable — error counts, file
|
|
543
|
+
names and line numbers all SURVIVE, so `30 errors` → `22 errors` is progress and not a
|
|
544
|
+
repeat), and the gap the worker sees is never rewritten; "wrote" is the **count delta of the
|
|
545
|
+
gate audit's allow-decision write/edit records** (the F32 instrument — never `git status`,
|
|
546
|
+
never a tree diff, and never the path SET, which is constant after iteration 1 when a step
|
|
547
|
+
rewrites one target); strikes are **sticky** — a good iteration in between never repays one.
|
|
548
|
+
Everything else that bounds a step is unchanged and each still ends it on its own authority:
|
|
549
|
+
the wallet, the wall, the variance meter and the stall fuse. The ceiling is **arbiter
|
|
550
|
+
territory** — the plan cannot express it, tighten it or raise it — and the practical
|
|
551
|
+
consequence for a caller is that a **converging step now runs on the money and wall you
|
|
552
|
+
signed** instead of stopping at a count, so size those two numbers for the job (the ladder
|
|
553
|
+
adds no third ceiling of its own). The exhaustion terminal is the same `cap-halt` category
|
|
554
|
+
as before; the spine gains a per-iteration `ladder` record
|
|
555
|
+
(`{iteration, strike, strikes, limit, wrote, repeatOf, distinctGaps}`), and the `cap-halt`,
|
|
556
|
+
escalation and wall-stop records carry `strikes`/`strikeLimit`/`distinctGaps` beside the
|
|
557
|
+
iteration count rather than a cap the step no longer has. **The replan brief
|
|
558
|
+
names the mechanism**: which shape ended the step (converging-cut / stalled-no-write /
|
|
559
|
+
repeated-gap, with the iteration numbers as evidence), the gap trajectory, and how much money
|
|
560
|
+
and wall the stop left unspent (time reported UNBOUNDED when no wall was set, never `0`). It
|
|
561
|
+
names no culprit file — F28 applies to the replan channel too.
|
|
562
|
+
|
|
563
|
+
**The close TREND rule — how the CLOSE-FIX LOOP ends (PRD v1.46 §4).** `capRuns` retired as
|
|
564
|
+
that loop's governor once its own $0 replay over every archived fix loop came back clean (0
|
|
565
|
+
greens harmed — all three historical fix-loop greens converted in ≤ 2 verdicts; 1 real waste
|
|
566
|
+
case caught, dead flat at 2 errors for 7 verdicts until the wall). It now stops on the same
|
|
567
|
+
2-strike no-progress rule, read off a DIFFERENT signal: the close's own graded numbers, **per
|
|
568
|
+
stage** (`src/trend.js`). A stage's series is the first number on the first red-marked line of
|
|
569
|
+
its output, compared only against that stage's own BEST so far (never last-only, the same
|
|
570
|
+
oscillator reason the ladder keeps a seen-set); reaching a LATER stage than ever before is
|
|
571
|
+
progress too, since a staged close is first-red-wins. Two consecutive comparable readings with
|
|
572
|
+
nothing improving ends the loop, under the unchanged `cap-halt` terminal and the unchanged
|
|
573
|
+
`escalated` outcome. **Never across stages** — merging a staged close's axes into one series
|
|
574
|
+
reads a real see-saw (suppressions 12 → 5 while the type errors it re-exposed went 2 → 0) as a
|
|
575
|
+
regression, and that mistake is inexpressible here rather than avoided by care. A stage whose
|
|
576
|
+
output carries no number donates NOTHING — not a zero, not a strike (F6): while the instrument
|
|
577
|
+
cannot compare, `capRuns` is what bounds the loop. It bounds the **consecutive run of
|
|
578
|
+
unreadable gradings**, not the loop's opening — it lifts the moment a stage reports a
|
|
579
|
+
comparable number and **re-arms the moment one stops**, so a loop that reads, goes blind and
|
|
580
|
+
reads again is never bounded by it, while a loop that never compares anything binds on exactly
|
|
581
|
+
the iteration the fixed count always bought it (F84: gating it on "has this loop ever compared
|
|
582
|
+
anything" made a single stage ADVANCE disarm it for the rest of the run, and a numberless-red
|
|
583
|
+
close then had money and the wall for a bound). Two known accepted limits, both stated at the
|
|
584
|
+
site: (1) "lower is better" is not derivable from prose, so a floor-shaped stage (`N tests
|
|
585
|
+
executed, below the seed's M`) reads as not-improving rather than converging — the fail-safe
|
|
586
|
+
direction, since a false "flat" costs one conservative stop and a false "converging" costs a
|
|
587
|
+
top-up spent on a dead run; (2) a series is one bucket per stage NAME, and a stage name is not
|
|
588
|
+
an axis — a close free to red one stage on two different populations (`N error(s) in <scope>`
|
|
589
|
+
vs `the target files are clean but M error(s) exist outside them`) donates both genres to one
|
|
590
|
+
series, and a run crossing that seam can read converging on work that only swapped which wall
|
|
591
|
+
it is behind. Neither is sharpened by teaching the reader to tell prose shapes apart (the F49
|
|
592
|
+
precedent); the second's root fix is a stage split in the close, which is the spec's to change.
|
|
593
|
+
The spine gains a per-iteration `ladder` record here too; every reading names its `governor`
|
|
594
|
+
(`step-ladder` | `close-trend`) so two instruments under one event type can never be averaged
|
|
595
|
+
into one number.
|
|
596
|
+
|
|
597
|
+
**A MONEY halt is decision-ready (PRD v1.46 §2).** A cut by the wallet is the same KIND of
|
|
598
|
+
stop the wall is, so it now reads out the same way: the verdict already minted is KEPT (never
|
|
599
|
+
discarded, never re-derived), and a `money-halt` spine record carries `budgetUsd`,
|
|
600
|
+
`remainingUsd` **with its own `spendComplete`** (the remaining is `budget − spent`, so on a run
|
|
601
|
+
whose spend is a FLOOR it is a CEILING and says so rather than reading as the number — the same
|
|
602
|
+
duty `wall-halt` has carried since W-2; it is the ledger's one figure, never re-derived here),
|
|
603
|
+
the kept `verdict`/`stage`, the run's own per-stage `trend`
|
|
604
|
+
(`converging | flat | unknown`) with the `reading` and `series` it judged, and three
|
|
605
|
+
`options` — top up and resume, revise the spec, abandon. Emitted at every site that cuts a run
|
|
606
|
+
on money (the step loop, the close-fix loop, and the scout/draft relay), exactly as
|
|
607
|
+
`wall-halt` is. The trend obeys the accuracy law above and says `unknown` out loud when the
|
|
608
|
+
close rendered no number (F6). **The library only ever REPORTS**: `budgetUsd` is in the spec
|
|
609
|
+
hash, so a top-up is a spec edit a human signs — nothing in a run may widen its own budget.
|
|
610
|
+
|
|
611
|
+
**ONE progress instrument, read by both halts.** The wall halt used to run its own: byte
|
|
612
|
+
equality of the last two close gaps, reading `stalled` / `moving` / `unknown` beside the money
|
|
613
|
+
halt's `flat` / `converging` / `unknown` on the same run. Both now read `runTrend`, the run's
|
|
614
|
+
own `src/trend.js` reader. Where a stage reported a comparable NUMBER it decides; where none
|
|
615
|
+
did, the byte comparison survives INSIDE `unknown` as a separate `motion` field
|
|
616
|
+
(`changed | unchanged | null`) and as a clause in the `reading` — never as the headline
|
|
617
|
+
verdict, because "the close's output changed" is not "the run got better" (F6 in a trend's
|
|
618
|
+
coat), and never mapped onto `trend`, because that would put the promotion on the spine.
|
|
619
|
+
`unchanged` points the lever at the goal; `changed` points it at reading the last close output.
|
|
620
|
+
The two readers stay SEPARATE INSTANCES for separate questions (`src/trend.js`, "ONE PER
|
|
621
|
+
SERIES"): the halt readouts ask *was the RUN converging when its allowance cut it*, the
|
|
622
|
+
close-fix loop's governor asks *is THIS loop out of ideas*.
|
|
623
|
+
|
|
624
|
+
**A resumed leg's readout judges the whole CHAIN.** `readResume`'s `restart.grades` carries the
|
|
625
|
+
dead leg's recorded close grades forward — `[{stage, value}]`, counts and stage names only,
|
|
626
|
+
never a gap byte — and `runJob`/`runPlan` take them as `resumeGrades`, which seed the run
|
|
627
|
+
trend's BASELINES (`createTrend({seed})`). Without them a resumed leg restarts the trend at its
|
|
628
|
+
own first close and can report `flat` — *revise the goal* — on a run that was converging when
|
|
629
|
+
its allowance ran out. The seed folds into the series and the per-stage best and into nothing
|
|
630
|
+
else: no iteration counted, no strike minted, no `comparableEver`, no stage position — it is
|
|
631
|
+
history, not this leg's evidence, so the blind-cap backstop still governs a leg whose own
|
|
632
|
+
readings never compare. The close-fix loop's governor is deliberately NOT seeded (it is the
|
|
633
|
+
leg-local question). Sources, in order: the `ladder` records the live governor already wrote
|
|
634
|
+
(`governor: 'close-trend'`, taken verbatim), the `close-precheck`/`outer-close` records, and —
|
|
635
|
+
only on a spine predating that governor — the `close-verdict` records after the `fix-loop`
|
|
636
|
+
marker, which is the same population and excludes a STEP's failing exit. Known limit: one spine
|
|
637
|
+
is read, so a resume of a resume inherits the previous leg's chain and not the one before it.
|
|
638
|
+
|
|
494
639
|
**Time and materials (T + A, PRD v1.27/v1.29).** `runPlan` starts ONE wall clock per run
|
|
495
640
|
(`createClock`, `src/clock.js`) from the signed `maxWallMs` and emits `wall-clock` with the
|
|
496
641
|
requested AND enforced numbers up front, plus the `closeStages` count that explains the gap
|
|
@@ -499,17 +644,25 @@ stage count comes from `stageClose`, the same staging the runner executes). Enfo
|
|
|
499
644
|
**between-round deadline** — the only seam that exists, since `loop.stop()` cannot cut an
|
|
500
645
|
in-flight call (F61: fired at 500ms,
|
|
501
646
|
returned at 4,018ms) — so an attempt that crosses the deadline mid-flight emits `wall-bounded`,
|
|
502
|
-
is judged, and feeds its gap forward exactly like a round-bounded attempt.
|
|
647
|
+
is judged, and feeds its gap forward exactly like a round-bounded attempt. A call that never
|
|
648
|
+
returns is bounded by the two per-call numbers the same clock derives and `runPlan` passes to
|
|
649
|
+
every provider call: `timeoutMs` (BA-18, idle — reset by every byte) and `deadlineMs` (BA-19,
|
|
650
|
+
TOTAL duration — reset by nothing, so it is the bound for a stream that trickles forever, F66's
|
|
651
|
+
274-minute call). Both are the wall's own remainder read at call time, and `deadlineMs` is
|
|
652
|
+
**omitted entirely** on a time-unbounded run — no wall, no derived ceiling (F45). Because both
|
|
653
|
+
came from the wall, a trip past the cap routes `wall-halt`, never `provider-red`; the
|
|
654
|
+
discriminator is `clock.expired()` and never the error code, so a timeout with time still on the
|
|
655
|
+
clock stays the transport casualty it is (F64). **The close is never
|
|
503
656
|
bounded by the wall and always runs to completion** — a deadline that kills grading leaves the
|
|
504
657
|
run unreadable after the money is spent (the F45 class, money generalized to time); what the
|
|
505
658
|
wall stops is the START of new work. Three sites decide the run-level terminal `wall-halt`, all
|
|
506
659
|
on the same clock: the step loop after a step returns, a step that would BEGIN already expired,
|
|
507
660
|
and the close-fix loop before it opens another iteration — which then stops on the verdict the
|
|
508
661
|
last close already minted (hamr: *"when time is up, keep the grade we already have and stop"*),
|
|
509
|
-
carrying that verdict, the iterations spent, and
|
|
510
|
-
`
|
|
511
|
-
(resume-to-cap: the stop IS the checkpoint) and revising the goal — both
|
|
512
|
-
hash to re-sign. `cutMidCall` on the `wall-halt` record splits the deadline seen INSIDE a
|
|
662
|
+
carrying that verdict, the iterations spent, and the SAME progress reading the money halt
|
|
663
|
+
carries (`trend`: `converging | flat | unknown`, plus `motion`) so the human can pick between
|
|
664
|
+
raising `maxWallMs` (resume-to-cap: the stop IS the checkpoint) and revising the goal — both
|
|
665
|
+
spec edits, both a new hash to re-sign. `cutMidCall` on the `wall-halt` record splits the deadline seen INSIDE a
|
|
513
666
|
provider call from the deadline read between them. Time reporting is
|
|
514
667
|
licensed at ~90% accuracy by ruling: imprecision is fine, reporting an unknown or unbounded
|
|
515
668
|
duration as `0` never is (F6 extended to time — unbounded reports `null`, never `Infinity`).
|
|
@@ -587,6 +740,334 @@ audit, the smoke store and the litectx store are denied — the agent neither au
|
|
|
587
740
|
arbiter nor reads its books. N2 bounds (honest): `gold`/`rubric` closes refuse
|
|
588
741
|
`close-unsupported` (execution lands at N4).
|
|
589
742
|
|
|
743
|
+
### The reuse registry (Layer 3) — `src/bridges.js`, `src/selection.js`
|
|
744
|
+
|
|
745
|
+
A **bridge** is the plan a green actually executed, kept so the next run of the same SHAPE
|
|
746
|
+
starts from it instead of cold. Storage is a directory of plain JSON files at an
|
|
747
|
+
**operator-supplied path** — no database, no default location (a missing registry reds
|
|
748
|
+
rather than being conjured). Everything here is either a pure derivation or an explicit
|
|
749
|
+
read/write of one file; nothing decides a run.
|
|
750
|
+
|
|
751
|
+
**Writing the box (R1 — only a green writes it).** `mintBridge(meta, record)` builds a new
|
|
752
|
+
entry from a FIRST green — there is deliberately no way to create one without a green, so a
|
|
753
|
+
failed plan cannot enter the registry even by accident. `appendGreen(bridge, record)` adds
|
|
754
|
+
the plan AS EXECUTED as the next **version** plus a history row; `appendRed(bridge, record)`
|
|
755
|
+
writes a **history row only** and never touches `versions` — a red demotes, it does not edit
|
|
756
|
+
the recipe. Both are pure (a new entry is returned; the input is untouched).
|
|
757
|
+
`saveBridge(dir, bridge)` writes atomically (temp + rename) and REFUSES an entry its own
|
|
758
|
+
validator rejects; `loadBridge` / `loadRegistry` read back, and a malformed file is
|
|
759
|
+
**skipped and reported** (`ok:false` with one red per bad file) rather than silently
|
|
760
|
+
dropped. `makeRegistry` is separate from `saveBridge` on purpose: saving must never conjure
|
|
761
|
+
a registry from a typo'd path.
|
|
762
|
+
|
|
763
|
+
**Status is DERIVED, never stored** (`deriveStatus`): **candidate** = one green, **proven** =
|
|
764
|
+
greens on ≥2 DISTINCT patients, and a `red` on a proven entry drops it back to candidate.
|
|
765
|
+
A **casualty is not a red** — only the literal outcome `red` demotes; `provider-red`,
|
|
766
|
+
`wall-halt`, `close-crashed` and friends are casualties and are never evidence in either
|
|
767
|
+
direction. There is deliberately **no probability score**: n=1 is an anecdote whichever way
|
|
768
|
+
it points, and a percentage over 2–3 runs is fake precision. `costUsd`/`wallMs`/`rounds` are
|
|
769
|
+
a number or an EXPLICIT `null`, and the key is REQUIRED either way, so an unknown has to be
|
|
770
|
+
said rather than omitted (F6 — an omitted key is how a `?? 0` gets written downstream).
|
|
771
|
+
|
|
772
|
+
**`loadGate(bridge, job)` → `{ ok, reds }` — the load-time gate.** Exactly three checks,
|
|
773
|
+
asked *"is this the right KIND of recipe?"*: the job's **verdict type**, the **close-stage
|
|
774
|
+
kinds** (v1: the same stage names in the same order), and every stored verb **within THIS
|
|
775
|
+
job's signed menu** (an omitted `tools` means the concrete current `TOOL_MENU`, MED-1).
|
|
776
|
+
**Nothing about paths, scopes or targets** — those are instance-bound and are EXPECTED to
|
|
777
|
+
red: every recipe from a different day names yesterday's bricks, and that is what a recipe
|
|
778
|
+
IS. The full `validatePlan` still judges the TWEAKED draft at draft time; no second, looser
|
|
779
|
+
path exists for an inherited plan. A failing gate is a RESULT (`recipe-stale`), never a throw.
|
|
780
|
+
|
|
781
|
+
**A CHANGED CLOSE therefore expires every bridge minted under the old one.** The stored
|
|
782
|
+
`closeStageNames` must match the job's stage names in the same order, so adding, renaming,
|
|
783
|
+
splitting or reordering a stage makes existing entries fail the gate by construction. That is
|
|
784
|
+
the doctrine, not a rough edge: a different close is a different KIND of job, and a recipe
|
|
785
|
+
proven against one ruler has not been proven against another. The remedy is a fresh green under
|
|
786
|
+
the new close, which mints the next version — an entry is **never edited into agreement**, and
|
|
787
|
+
falling back to cold stays the caller's decision. In this repo the `u-*` close split
|
|
788
|
+
(2026-08-05) put `aurora-u-spawner` and `litectx-u` in exactly that state; both are re-minted by
|
|
789
|
+
their next green, not repaired.
|
|
790
|
+
|
|
791
|
+
**`runJob`/`runPlan` take `bridge?`** — a validated entry to reuse. The gate runs **at the
|
|
792
|
+
door**: before the clock, before the close precheck, before any token. On a pass, the
|
|
793
|
+
**newest** version's plan rides into the FIRST drafting prompt as a starting draft
|
|
794
|
+
(`bridge-loaded {name, versions, runid}` on the spine) and everything after it is the
|
|
795
|
+
ordinary shipped path — same validator, same redraft-on-reds, same ONE-replan ceiling, and a
|
|
796
|
+
**replan never re-injects the bridge** (it drafts from the run's own state). On a fail the
|
|
797
|
+
run returns the distinct terminal **`recipe-stale`** with `bridge-gate {outcome, name, reds}`
|
|
798
|
+
and a decision-ready escalation, having spent nothing. **Falling back to cold is the
|
|
799
|
+
caller's decision, not an automatic silent fallback** — starting a paid run on a decision
|
|
800
|
+
nobody made is the same class of error as widening a cap to manufacture a green. Omit
|
|
801
|
+
`bridge` and the flow is byte-identical to a pre-Layer-3 run: no new event, and no starting
|
|
802
|
+
draft anywhere in the prompt.
|
|
803
|
+
|
|
804
|
+
**`renderListing(registry)` / `selectionPrompt(listing, ask)` — `src/selection.js`.** Pure
|
|
805
|
+
text: they read no file, call no model and decide nothing. `renderListing` takes
|
|
806
|
+
`loadRegistry`'s result (or a bare array) and renders one compact block per bridge — name,
|
|
807
|
+
the goal sentence it greened, status, greens/reds with the last outcome, and the cost/time
|
|
808
|
+
BAND of its greens. An unknown cost or duration renders `UNKNOWN` and a partial aggregate
|
|
809
|
+
says how many it skipped (never a `$0.00` that reads as exact); entries that could not be
|
|
810
|
+
read are NAMED, so the listing can never be quietly shorter than the directory. Ordering is
|
|
811
|
+
deterministic by name and it never throws. `selectionPrompt` wraps that listing with the ask
|
|
812
|
+
and D3's two standing rules — *"none matches"* is a first-class answer that means the agent
|
|
813
|
+
drafts new, and a PINNED workflow may be refused only EXPLICITLY, with a reason. The
|
|
814
|
+
selection CALL, the pin/shortlist/force-cold flow and the parse of the answer are YOURS —
|
|
815
|
+
or `runReuse` below, which is the shipped one.
|
|
816
|
+
|
|
817
|
+
### The reuse ENVELOPE and `runReuse` (Layer 3, D7) — `src/reuse.js`
|
|
818
|
+
|
|
819
|
+
`runReuse` is `runJob` under an operator-signed **envelope**: try a stored workflow, then
|
|
820
|
+
another, then draft cold — hamr's *"$5 and 30 mins x2 then start anew"*. It composes; it
|
|
821
|
+
decides nothing the arbiter owns.
|
|
822
|
+
|
|
823
|
+
**`validateEnvelope(input, { job? })` → `{ ok, reds, envelope }`.** The envelope is
|
|
824
|
+
`{ perTryBudgetUsd, perTryWallMs, bridgeTries }` and **all three are required and
|
|
825
|
+
explicit — there is no default for any of them**, for `maxWallMs`'s reason: a defaulted
|
|
826
|
+
cap is a silent second ceiling. `bridgeTries: 0` is legal and means force-cold. With a
|
|
827
|
+
`job`, the composition is checked: the per-try numbers may only **TIGHTEN** the signed
|
|
828
|
+
spec — a per-try budget above `job.budgetUsd`, or a wall above `job.maxWallMs`, is
|
|
829
|
+
`envelope-widens`, never a silent raise. (A spec with no `maxWallMs` is time-unbounded by
|
|
830
|
+
explicit choice, so any wall tightens it.)
|
|
831
|
+
|
|
832
|
+
**`resolveTrySpec(job, envelope)` → the per-try spec.** Every try — cold leg included —
|
|
833
|
+
runs THIS spec, so the envelope's numbers are the numbers all the way down. Tightening
|
|
834
|
+
makes a genuinely different spec, so it makes a different **spec hash**: the tightened run
|
|
835
|
+
is a new spec VERSION and `runJob`'s approval gate refuses it until the operator signs
|
|
836
|
+
that hash. `runReuse` hands `approvals` straight through and has no way to forge one. An
|
|
837
|
+
envelope equal to the spec's own numbers leaves this spec hash-identical.
|
|
838
|
+
|
|
839
|
+
**`resolveReuse(job, envelope)` → `{ schema: 'reuse-v1', spec, bridgeTries }`, and
|
|
840
|
+
`reuseSpecHash(resolved)` → the hash your operator signs. Show THIS one at your approval
|
|
841
|
+
gate** (`scripts/run-reuse.mjs` does). The envelope is three numbers and the third is a
|
|
842
|
+
MULTIPLIER — the worst case a reuse run authorizes is
|
|
843
|
+
`perTryBudgetUsd × (bridgeTries + 1)` — so a signature over the per-try spec alone covers
|
|
844
|
+
only budget and wall, and `--tries 0` and `--tries 9` print the same hash. The signed
|
|
845
|
+
artifact is therefore the per-try spec WRAPPED with the count. It is a wrapper rather than
|
|
846
|
+
a field on the spec because `validateJob` reds unknown top-level fields: a `bridgeTries`
|
|
847
|
+
written onto the spec would flip the hash and then make every try a `job-red`. The hash is
|
|
848
|
+
composed from `jobSpecHash` (so MED-1's resolved-`tools` pinning is inherited, with no
|
|
849
|
+
second canonicalizer to drift) under a `reuse-v1` domain prefix, and `runReuse` reads its
|
|
850
|
+
loop bound back off the same resolved object — the count enforced is the count signed.
|
|
851
|
+
**This is a signing-SCHEME change: any hash signed before the count was folded in no
|
|
852
|
+
longer matches, by design, and there is no legacy acceptance path — re-sign once.** Your
|
|
853
|
+
gate still mints `approvals` for `runJob` at the PER-TRY SPEC's `jobSpecHash`, which the
|
|
854
|
+
matching approval hash entails (it is a function of exactly that hash plus the count).
|
|
855
|
+
|
|
856
|
+
**`selectBridge({ registry, job, ask, provider, pinned?, shortlist?, forceCold?, exclude? })`.**
|
|
857
|
+
ONE model call on the drafter-tier provider **you** hand in (no provider is constructed
|
|
858
|
+
inside), rendering `renderListing` + `selectionPrompt` and parsing a strict
|
|
859
|
+
`{"choice": <name|null>, "reason": "…"}` through the repo's one `extractArtifact`. Returns
|
|
860
|
+
`{ choice, reason, called, refused, forcedCold, red, costUsd, spendComplete, candidates }`.
|
|
861
|
+
`forceCold` and an empty candidate set skip the call entirely ($0, no tokens). A name that
|
|
862
|
+
is not on the listing it was given is a named red, never used. **A pin does not bypass the
|
|
863
|
+
call** (D3): it is stated in the prompt, and if the answer names anything else the result
|
|
864
|
+
is `{refused: true}` — the substitute is NOT adopted and the decision goes back to you.
|
|
865
|
+
Cost is metered and reported: `costUsd` is a number or an explicit `null` (F6).
|
|
866
|
+
|
|
867
|
+
**`runReuse({ job, approvals, registryDir, envelope, patient, workdir, provider, emit, … })`.**
|
|
868
|
+
Envelope → registry → up to `bridgeTries` tries → the cold leg. Per try: selection runs
|
|
869
|
+
against a **freshly reloaded** registry with the already-tried names excluded, the chosen
|
|
870
|
+
entry rides into `runJob` (whose own load gate is the D2 shape check — a `recipe-stale`
|
|
871
|
+
refusal costs $0 and simply moves to the next candidate), and then **the box is written
|
|
872
|
+
(R1)**: a green appends a VERSION carrying the plan AS EXECUTED (read from the run's own
|
|
873
|
+
`plan-accepted`) plus a history row; a graded red appends a history row only; a casualty
|
|
874
|
+
appends a row under its **own** outcome name and cannot demote. **A green ends the loop.**
|
|
875
|
+
Tries exhausted → a cold run under the same per-try numbers, whose green mints a new
|
|
876
|
+
bridge named for the job slug (or appends, if that name already holds greens — clobbering
|
|
877
|
+
a green is exactly what R1 exists to prevent). A cold RED writes nothing: the entry bar is
|
|
878
|
+
a green.
|
|
879
|
+
|
|
880
|
+
**Which outcomes are a graded RED** (`REUSE_GRADED_RED`): only `escalated` — the terminal
|
|
881
|
+
where the close judged the tree, the bounded fix loop spent its attempts with money still
|
|
882
|
+
on the table, and the close was still red. Everything else non-green is a **casualty** and
|
|
883
|
+
keeps its own name: `cap-halt`/`wall-halt` (governance), `provider-red`/`step-stalled`
|
|
884
|
+
(transport), `close-red`/`close-unsupported` (the close FAULTED and rendered no judgment),
|
|
885
|
+
`recipe-stale` (refused at the door), `plan-red`/`step-red:*`/`check-red` (the flow stopped
|
|
886
|
+
before the close ever judged). All of them are recorded in full on the history row, which
|
|
887
|
+
is where D6 puts the detail a human reads; the coarse status ladder is left alone.
|
|
888
|
+
|
|
889
|
+
**Result:** `{ outcome, tries[], selection[], spentUsd, spendComplete, bridgeWrites[],
|
|
890
|
+
decision, options, detail, reds, triesUsed, triesAuthorized, envelope, specHash,
|
|
891
|
+
approvalHash }` — `specHash` is the per-try spec's (what `runJob`'s own gate reads),
|
|
892
|
+
`approvalHash` is the operator's, covering all three envelope numbers. Every
|
|
893
|
+
try row is **decision-ready**: `bridge`, `runOutcome`, `verdictClass`, `failingStage`,
|
|
894
|
+
`spentUsd` vs `capUsd`, `wallMs` vs `wallCapMs`, `capBound`, `wallBound`, `rounds`, and
|
|
895
|
+
`closeReached`. `spentUsd` is the sum of PRICED figures only across tries AND selection
|
|
896
|
+
calls; one unknown makes `spendComplete` false, and a try whose `job-end` never landed
|
|
897
|
+
reports `spentUsd: null` — never a `0` that reads as exact (F6). On a RESTARTED try,
|
|
898
|
+
`rounds` folds both attempts exactly as `spentUsd` and `wallMs` do (the row states one span,
|
|
899
|
+
not a count from one leg against money from two), and the fold is declared on `try-start` as
|
|
900
|
+
`priorRounds`; a leg whose turns cannot be counted leaves it `null` and the row keeps the
|
|
901
|
+
unknown rather than reporting the half it can see.
|
|
902
|
+
|
|
903
|
+
> **The selection calls sit OUTSIDE the per-try caps**, by construction: they happen
|
|
904
|
+
> before a try starts, so no try's ledger can hold them. They are small (one short prompt
|
|
905
|
+
> each, no tools) and they ARE metered — every one lands in `selection[].costUsd` and in
|
|
906
|
+
> the total — but a reader budgeting `perTryBudgetUsd × (bridgeTries + 1)` should know
|
|
907
|
+
> that is the caps' sum, not the run's ceiling. Named here rather than folded in: an
|
|
908
|
+
> advertised number the run does not enforce is the thing this repo does not ship.
|
|
909
|
+
|
|
910
|
+
> **F45, and why there is no threshold here.** A try's budget must fund **the attempt PLUS
|
|
911
|
+
> its close** — a cap that dies mid-grading produces an unreadable row, and an unreadable
|
|
912
|
+
> row is a casualty, not evidence. Nothing in this process can know what a close costs (it
|
|
913
|
+
> is your command, at your price), so **no threshold is invented** — threshold-setting is
|
|
914
|
+
> arbiter territory, set from a measured base rate, never fitted to the sample in hand.
|
|
915
|
+
> What the module does instead is make the bind VISIBLE: `closeReached: false` next to
|
|
916
|
+
> `capBound: true` on a row IS the diagnosis. Size the envelope with that in hand.
|
|
917
|
+
|
|
918
|
+
Fallthrough to the next bridge, and to cold, is automatic **only because the envelope
|
|
919
|
+
pre-authorized it**. Nothing else is: a refused pin (`selection-refused`), an unusable
|
|
920
|
+
selection answer (`selection-red`), a missing registry (`registry-red`) and an
|
|
921
|
+
envelope that will not compose (`envelope-red`) all STOP with a decision-ready escalation
|
|
922
|
+
and hand the call back to you. So do the three answers **no further try could change** —
|
|
923
|
+
`unapproved-spec` (a tightened envelope makes a new spec version, and it is signed, not
|
|
924
|
+
inherited: the decision hands you the exact hash), `job-red`, and `smoke-red` — which end
|
|
925
|
+
the run on the try that hit them rather than reproducing themselves down the whole
|
|
926
|
+
envelope. Every category this emits is in the ledger's executable excluded-set, so none of
|
|
927
|
+
them lands as an unclassified library bug.
|
|
928
|
+
|
|
929
|
+
`scripts/run-reuse.mjs` is the reference operator runner: `--job`, `--registry`, and the
|
|
930
|
+
three envelope numbers `--budget` / `--wall` / `--tries`, plus optional `--pin` /
|
|
931
|
+
`--force-cold`, with the approval gate on `reuseSpecHash` (all three numbers — a hash
|
|
932
|
+
signed for a different `--tries` is refused, and says so), the F67 outside watchdog
|
|
933
|
+
(sized for the SUM of the tries, not one), the seed refusal, the gate-audit relocation and
|
|
934
|
+
the secrets scan.
|
|
935
|
+
|
|
936
|
+
#### Resuming a killed run (module C)
|
|
937
|
+
|
|
938
|
+
A reuse run is up to `tries + 1` full jobs long, so a kill mid-run is a real event and it
|
|
939
|
+
must not cost the whole envelope. **`readResume(events, {deathAt?, direct?, resumableOutcomes?})`**
|
|
940
|
+
reads the dead run's own spine back into the state a resume continues from: `{ started,
|
|
941
|
+
approvalHash, completed[], tried[], restart (with its step-level `seed` and its close
|
|
942
|
+
`grades`), r1Missing, spentUsd,
|
|
943
|
+
spendComplete, carrySpentUsd, carrySpendComplete, ended, greened, … }`. Hand that object to
|
|
944
|
+
**`runReuse({…, resume})`** and the run picks up where it stopped.
|
|
945
|
+
|
|
946
|
+
Two options widen WHICH spines it reads, both OFF by default so the reuse loop's own
|
|
947
|
+
semantics are byte-unchanged (PRD v1.46 §3):
|
|
948
|
+
|
|
949
|
+
- **`direct: true`** reads a plain `runJob` spine — no envelope, no try windows — as ONE
|
|
950
|
+
implicit try opened at `job-start`, through the same window machinery rather than a second
|
|
951
|
+
reader. Without it such a spine is honestly refused (`started: false`), which is what a
|
|
952
|
+
reuse runner needs. `job-start` carries the DECLARED fold (`priorSpentUsd` with its
|
|
953
|
+
`priorSpendComplete`, and `priorWallMs`, each present only when there is one) exactly as
|
|
954
|
+
`try-start` does, so a chain of resumes adds only each attempt's own new rounds instead of
|
|
955
|
+
re-deriving and double-billing. **An inherited FLOOR is one** even when the money it qualifies
|
|
956
|
+
is `$0`: a leg whose every round came back unpriced folds nothing countable and is still not
|
|
957
|
+
exact, so the declaration is emitted to carry that unknown forward rather than letting it read
|
|
958
|
+
as an exact zero (F6).
|
|
959
|
+
- **`resumableOutcomes: ['cap-halt', 'wall-halt']`** reclassifies a landed `job-end` whose
|
|
960
|
+
outcome is a GOVERNANCE HALT as a restart rather than a completed row. "A landed job-end is
|
|
961
|
+
complete" is true of a VERDICT; a halt means an operator-owned allowance ran out with the
|
|
962
|
+
work on disk and the plan on the spine. A green or any red stays non-resumable under every
|
|
963
|
+
setting — a verdict already rendered is never re-bought.
|
|
964
|
+
|
|
965
|
+
`scripts/run-u.mjs --resume <runid|path>` is the operator-side consumer: it skips the patient
|
|
966
|
+
reset (`resumeTreeGate` instead — a dirty tree is what a resume expects; only a moved HEAD
|
|
967
|
+
stops it), folds the halted run's money and wall in, re-enters at the checkpoint, and arms the
|
|
968
|
+
outside watchdog on the REMAINING wall. The top-up itself stays a spec edit the human signs.
|
|
969
|
+
**A zero wall remainder REFUSES the launch** (exit 2, before the key and before the patient):
|
|
970
|
+
W-2 says a run out of time keeps the grade it has and stops, so a resume with no time to start
|
|
971
|
+
anything in is a decision, not an unbounded launch — and launching it used to hand the outside
|
|
972
|
+
watchdog `--wall-ms 0`, which it defaulted to `null` and armed no deadline at all under a banner
|
|
973
|
+
still advertising one. The refusal names the lever: raise `maxWallMs`, which moves the spec hash
|
|
974
|
+
and is re-signed.
|
|
975
|
+
|
|
976
|
+
- **Completed tries are not re-run.** They come back as rows (marked `inherited`) and
|
|
977
|
+
their bridges stay excluded from every later selection. A try whose `job-end` landed —
|
|
978
|
+
the close judged it — counts as completed even if the kill beat its `try-end`; paying
|
|
979
|
+
again for a verdict already rendered is exactly what resume exists to avoid. If the
|
|
980
|
+
kill also beat the registry write, that row is LOST and named (`r1Missing`); nothing is
|
|
981
|
+
invented in its place.
|
|
982
|
+
- **A mid-flight try RESTARTS** against the same workflow, with **no second selection
|
|
983
|
+
call** (that pick was made and paid for), and it is not counted as consumed.
|
|
984
|
+
- **It restarts at the STEP it died on, not at its beginning.** hamr's ruling: *"start
|
|
985
|
+
last step instead from the beginning, why would i want to waste more money on something
|
|
986
|
+
i already started"*. `restart.seed` is the finest checkpoint the dead spine can PROVE —
|
|
987
|
+
`{phase, plan, completedSteps[{id, seq, by}], planSeq}` — and it rides into `runJob`
|
|
988
|
+
(and on to `runPlan`) as `resumeSeed`:
|
|
989
|
+
- **scout: skipped** (`scout-skipped`, never silence). Its survey is not on the spine,
|
|
990
|
+
so the one consequence is stated rather than discovered: a replan after a resume
|
|
991
|
+
drafts from an empty survey plus its failure brief.
|
|
992
|
+
- **plan: reloaded, not re-drafted** — from the LAST `plan-accepted` in the window (a
|
|
993
|
+
replan emits its own, and the post-replan plan is the one that was running). It is
|
|
994
|
+
**re-validated** against the spec signed NOW; a plan that no longer validates is
|
|
995
|
+
`plan-red` by name (`resume-plan-red`) with nothing run. It is then re-emitted as this
|
|
996
|
+
leg's own `plan-accepted {phase:'resume'}`, because R1 mints a bridge version from the
|
|
997
|
+
plan AS EXECUTED read back off the spine.
|
|
998
|
+
- **completed steps: skipped**, in prefix order by id, each with its own `step-skipped`
|
|
999
|
+
record naming the event that proves it (`step-end{green}` — or an earlier
|
|
1000
|
+
`step-skipped`, so a resume of a resume does not re-pay for what the first one saved).
|
|
1001
|
+
Never a fake step-start/step-end pair; `plan-executed` records them as `skipped`.
|
|
1002
|
+
- **`phase: 'close'`** (an `outer-close` in the window) means every step is done: they
|
|
1003
|
+
are all skipped and the run goes straight to the close, which re-runs for no tokens.
|
|
1004
|
+
- A window with **no `plan-accepted`** (a scout/draft death) has no seed and simply
|
|
1005
|
+
restarts the try — nothing paid is re-payable there. If that window is itself an
|
|
1006
|
+
abandoned resume, the checkpoint is read from the newest abandoned window **of the
|
|
1007
|
+
same try** (measured on the real killed run: leg 3 died in its redraft, and reading
|
|
1008
|
+
only its own window would have discarded a checkpoint leg 2 paid $8.18 to reach).
|
|
1009
|
+
- The worker's conversation is **not** replayed — a transcript is not a checkpoint, and
|
|
1010
|
+
every attempt is fresh by the loop's own design.
|
|
1011
|
+
- **Its TREND baselines come with it.** `restart.grades` is the dead leg's close grades in
|
|
1012
|
+
order (`[{stage, value}]` — counts and stage names only, never a gap byte), and it rides
|
|
1013
|
+
into `runJob`/`runPlan` as `resumeGrades` beside `resumeSeed` — on the LIBRARY path
|
|
1014
|
+
(`runReuse` hands it to the restarted try itself) as well as through `run-u --resume`;
|
|
1015
|
+
`resume-start`'s `restart` states `gradesInherited`, a count, and only when there is one.
|
|
1016
|
+
It seeds the run trend's
|
|
1017
|
+
baselines so the resumed leg's `money-halt`/`wall-halt` readouts judge the CHAIN: a leg
|
|
1018
|
+
that re-grades an unchanged tree is flat on its own evidence while the RUN — what the
|
|
1019
|
+
top-up decision is actually about — may have been converging when the money ran out. See
|
|
1020
|
+
the trend section above for what the seed does and, deliberately, does not touch.
|
|
1021
|
+
- **Under the REMAINDER, never a fresh allotment.** The killed attempt's spend and wall
|
|
1022
|
+
FOLD IN (`runJob`'s `priorSpentUsd`/`priorSpendComplete`/`priorWallMs`, `createClock`'s
|
|
1023
|
+
`priorElapsedMs`), so
|
|
1024
|
+
the restart gets what is left of the signed per-try numbers. The caps are never
|
|
1025
|
+
rewritten — they are in the spec hash. An unfundable remainder is an honest `cap-halt`
|
|
1026
|
+
(or `wall-halt`, below one close timeout: a try that cannot fund its close produces an
|
|
1027
|
+
unreadable row) with **nothing launched**. Topping up is a new envelope, and a new
|
|
1028
|
+
envelope is a new signature.
|
|
1029
|
+
- **Money stays honest end to end.** `carrySpentUsd` (completed tries + selection calls)
|
|
1030
|
+
seeds the resumed ledger; the mid-try fold is deliberately NOT in it, because it comes
|
|
1031
|
+
back inside the restarted try's own terminal — counting it in both places would bill it
|
|
1032
|
+
twice. A try row's `spentUsd` is therefore the try's WHOLE spend across both attempts,
|
|
1033
|
+
and a declared floor stays a floor — through ALL four of `runJob`'s floor causes: a
|
|
1034
|
+
landed resumable `job-end` whose own `spendComplete` is `false` (a stall, or a call cut
|
|
1035
|
+
mid-flight) floors the restart fold exactly like a declared prior floor or an unpriced
|
|
1036
|
+
round does (F83's laundering finding, fixed the same day; the killed-mid-flight restart
|
|
1037
|
+
with no `job-end` at all is control-pinned unchanged).
|
|
1038
|
+
- **Refusals** (`resume-red`, in the ledger's excluded set): a seed signed under a
|
|
1039
|
+
DIFFERENT envelope than the one being enforced (both hashes are named), and a restart
|
|
1040
|
+
whose workflow has left the registry (substituting another would override a decision
|
|
1041
|
+
the runner did not make).
|
|
1042
|
+
- **The patient is CONTINUED, not reset** — `resumeTreeGate({head, seed, dirty})`: a dirty
|
|
1043
|
+
tree is the dead tries' real progress; only HEAD moving off the seed (a commit or rebase
|
|
1044
|
+
under the dead run) stops the resume.
|
|
1045
|
+
- **One run, one log:** the resumed process appends to the dead spine and passes
|
|
1046
|
+
`makeSpine(file, {startSeq})` so `seq` stays monotonic, keeps the dead run's runid (the
|
|
1047
|
+
R1 rows point at a spine FILE), and leaves the cumulative `gate-audit.jsonl` in the
|
|
1048
|
+
workdir to be relocated once at the end as usual.
|
|
1049
|
+
- **Known limit (F6):** a call killed BEFORE it returned wrote no event, so money it may
|
|
1050
|
+
already have been billed for is not on the spine and cannot be folded — the same limit
|
|
1051
|
+
every guard here has (a hung `generate()` leaves zero trace).
|
|
1052
|
+
|
|
1053
|
+
`scripts/run-reuse.mjs --resume <runid|spine path>` is the operator seam: it refuses a
|
|
1054
|
+
spine that is missing, corrupt mid-file, already terminal, or whose process is STILL
|
|
1055
|
+
ALIVE (pids from the run's `runner-start` record and the watchdog report, discriminated
|
|
1056
|
+
by `/proc/<pid>/cmdline`, because a live pid can be a recycled stranger), prints the whole
|
|
1057
|
+
reconstruction as a preview before you sign — including WHERE it picks up ("step 2 of 3
|
|
1058
|
+
\"verify-strict-typecheck\" — 1 step already finished and SKIPPED, not re-paid"), so the
|
|
1059
|
+
signature covers an attempt whose real cost you can see — archives the killed run's
|
|
1060
|
+
watchdog record so the readout cannot mistake it for this run's (defensively: a truncated
|
|
1061
|
+
report is not evidence of anything, and it can no longer abort a signed resume), and sizes the
|
|
1062
|
+
outside watchdog for the work that is actually LEFT. **Nothing left to run REFUSES the launch**
|
|
1063
|
+
(exit 2), with a lever per cause, because they are different asks: `NO WALL LEFT` when the
|
|
1064
|
+
restarted try burned its whole per-try wall (raise `--wall`), `NOTHING TO ARM` when every
|
|
1065
|
+
authorized attempt has already run (raise `--tries`, or read the run as it stands). Both are in
|
|
1066
|
+
the approval hash, so either is a new signature. Two smaller refusals in the same family: a flag
|
|
1067
|
+
given no value (`--tries` with nothing after it) stops rather than reading `Number('') === 0`
|
|
1068
|
+
and reshaping the run being signed, and `runner-start`'s recorded `argv` is redacted at the
|
|
1069
|
+
write site — the spine is append-only, so a scan after the bytes land is too late.
|
|
1070
|
+
|
|
590
1071
|
### `updateLedger({ ledgerFile, spineFiles })` → `{ appended, fold }` — `src/ledger.js`
|
|
591
1072
|
|
|
592
1073
|
The upstream ledger: spines fold into ONE append-only incident JSONL both the consumer
|
|
@@ -619,9 +1100,10 @@ panel reads the same file at N6.
|
|
|
619
1100
|
## Architecture
|
|
620
1101
|
|
|
621
1102
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
622
|
-
retry cap
|
|
1103
|
+
retry cap (a count on the close-fix loop, a shell-owned STRIKE ceiling on a plan step),
|
|
1104
|
+
verdict collection, escalation routing — stateless across runs; nothing inside
|
|
623
1105
|
negotiates with it. An **emergent middle**: the AGENT-authored plan — bounded steps, each
|
|
624
|
-
with its granted verbs, its round
|
|
1106
|
+
with its granted verbs, its round cap, its narrowed write scope and its
|
|
625
1107
|
form-checkable exits — schema-validated by `validatePlan` before tokens burn (the
|
|
626
1108
|
operator-authored `steps[]` shape is deleted, PRD v1.32). A **floor**: append-only
|
|
627
1109
|
JSONL spine (single source for every UI), litectx store per job, per-run ledger. Built on
|