bareloop 0.5.1 → 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 +1129 -1
- package/README.md +40 -13
- package/bareloop.context.md +799 -245
- package/package.json +3 -3
- package/src/bridges.js +571 -0
- package/src/clock.js +233 -0
- package/src/exits.js +8 -2
- package/src/index.js +52 -17
- package/src/job.js +231 -108
- package/src/ladder.js +196 -0
- package/src/ledger.js +55 -3
- package/src/plan.js +334 -20
- package/src/planrun.js +1519 -134
- package/src/ralph.js +373 -34
- package/src/reuse.js +1602 -0
- package/src/root.js +32 -12
- package/src/run.js +129 -391
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/stall.js +252 -0
- package/src/tools.js +611 -0
- package/src/trend.js +405 -0
- package/src/validate.js +35 -250
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +131 -0
- package/types/index.d.ts +7 -6
- package/types/job.d.ts +83 -8
- package/types/ladder.d.ts +86 -0
- package/types/ledger.d.ts +3 -1
- package/types/plan.d.ts +86 -7
- package/types/planrun.d.ts +142 -5
- package/types/ralph.d.ts +143 -7
- package/types/reuse.d.ts +388 -0
- package/types/root.d.ts +10 -2
- package/types/run.d.ts +56 -17
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/stall.d.ts +80 -0
- package/types/tools.d.ts +262 -0
- package/types/trend.d.ts +108 -0
- package/types/validate.d.ts +21 -38
- package/src/extract.js +0 -103
- package/src/interpret.js +0 -887
- package/types/extract.d.ts +0 -25
- package/types/interpret.d.ts +0 -201
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,1135 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.7.0] — 2026-08-05
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **`ralph()` now REFUSES a call with no exhaustion rule** (public API). `capRuns` became
|
|
12
|
+
optional in the signature when the `ladder` governor arrived — *"required unless `ladder` is
|
|
13
|
+
supplied"* — and nothing enforced the "unless". With neither, `1 <= undefined` is false: the
|
|
14
|
+
middle ran **zero** times and the call still returned `escalated` after emitting `cap-halt`
|
|
15
|
+
and an escalation reading `undefined/undefined runs spent, close still red` — a governance
|
|
16
|
+
stop narrated for a loop that was never entered. A param guard at entry now throws, naming
|
|
17
|
+
both alternatives (either one repairs the call). `capRuns` must be a positive integer;
|
|
18
|
+
`0`, `2.5`, `'3'`, `null` and `NaN` are refused the same way. **No shipped path changes** —
|
|
19
|
+
both in-library callers (the step loop and the close-fix loop) pass a `ladder`.
|
|
20
|
+
- **ONE progress instrument for both governance halts.** The wall halt used to run its own
|
|
21
|
+
reader — byte equality of the last two close gaps, reporting `stalled` / `moving` / `unknown`
|
|
22
|
+
beside the money halt's `flat` / `converging` / `unknown` on the same run. Two readers for one
|
|
23
|
+
question is how the two come to disagree, and the byte reader could only ever report MOTION —
|
|
24
|
+
that something changed, never which way. Both halts now read the run's own `src/trend.js`
|
|
25
|
+
reader. Where a stage reported a comparable NUMBER it decides (so a wall halt on a converging
|
|
26
|
+
run now says `converging`, with the series, instead of "it moved"); where none did, the byte
|
|
27
|
+
comparison survives INSIDE `unknown` as a new `motion` field (`changed | unchanged | null`)
|
|
28
|
+
and as a clause in the `reading`, with the lever adapting (`unchanged` → revise the goal;
|
|
29
|
+
`changed` → read the last close output). **Never promoted to a direction and never mapped onto
|
|
30
|
+
`trend`** — "the close's output changed" is not "the run got better" (F6 in a trend's coat).
|
|
31
|
+
W-2's semantics are untouched: the grade already minted is kept, the three levers are the same
|
|
32
|
+
three, and the close is still never bounded by the wall. **Spine change:** the `wall-halt`
|
|
33
|
+
record's `trend` now carries the money halt's vocabulary and a sibling `motion` field; no
|
|
34
|
+
`src/` or `scripts/` reader consumed the old values (grepped).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **A MONEY halt is decision-ready, exactly as W-2 made the wall one** (PRD v1.46 §2). A run
|
|
38
|
+
cut by its budget KEEPS the verdict already minted and emits a new `money-halt` spine record
|
|
39
|
+
carrying `budgetUsd`, `remainingUsd`, the kept `verdict`/`stage`, a progress `trend`
|
|
40
|
+
(`converging | flat | unknown`) with the `reading` and `series` it judged, and three
|
|
41
|
+
`options`: top up and resume, revise the spec, abandon. Emitted at every site that cuts a run
|
|
42
|
+
on money — the step loop, the close-fix loop, and the scout/draft relay. **The library only
|
|
43
|
+
reports**: `budgetUsd` is in the spec hash, so a top-up is a spec edit a human signs, and
|
|
44
|
+
nothing in a run may widen its own budget (unchanged hard line).
|
|
45
|
+
- **The close TREND instrument** (`src/trend.js`, internal). Reads a run's own close grades as
|
|
46
|
+
a series **per stage**, compared only against that stage's own best so far. Accuracy law:
|
|
47
|
+
never across stages — merging a staged close's axes reads a real see-saw (suppressions
|
|
48
|
+
12 → 5 while the type errors that scrub re-exposed went 2 → 0) as a regression. Never model
|
|
49
|
+
prose: the reading is the first number on the first red-marked line of the stage's output.
|
|
50
|
+
A stage that reports no number donates nothing — not a zero, not a strike, and the verdict
|
|
51
|
+
says `unknown` out loud (F6). Reaching a later stage than ever before counts as progress
|
|
52
|
+
(a staged close is first-red-wins). Known accepted limit, documented at the site: a
|
|
53
|
+
floor-shaped stage (`N tests executed, below the seed's M`) reads as not-improving rather
|
|
54
|
+
than converging — the fail-safe direction.
|
|
55
|
+
- **A resumed run's trend judges the WHOLE chain, not just the leg.** `readResume`'s `restart`
|
|
56
|
+
gains `grades`: the dead leg's close grades in order, as `[{stage, value}]` — counts and stage
|
|
57
|
+
names only, never a gap byte. `runJob`/`runPlan` take them as `resumeGrades` (beside
|
|
58
|
+
`resumeSeed`) and `createTrend` takes them as `seed`, folding them into the run trend's series
|
|
59
|
+
and per-stage bests. Without it a resumed leg restarts the trend at its own first close and can
|
|
60
|
+
report `flat` — *revise the goal* — on a run that was converging when its allowance ran out; a
|
|
61
|
+
leg that re-grades an unchanged tree is flat on its own evidence while the RUN is what the
|
|
62
|
+
top-up decision is about. The seed is history, not this leg's evidence, so it counts no
|
|
63
|
+
iteration, mints no strike, never sets `comparableEver` on its own (the blind-cap backstop
|
|
64
|
+
still governs a leg whose own readings never compare) and donates no stage position. The
|
|
65
|
+
close-fix loop's governor is deliberately **not** seeded — it answers the leg-local question
|
|
66
|
+
("is this loop out of ideas"), and seeding it rendered "still progressing" inside the terminal
|
|
67
|
+
of a loop that had just struck out flat. Grades are read from the `ladder` records the live
|
|
68
|
+
governor already wrote (`governor: 'close-trend'`, verbatim, never re-parsed), from
|
|
69
|
+
`close-precheck`/`outer-close`, and — only on a spine predating that governor — from the
|
|
70
|
+
`close-verdict` records after the `fix-loop` marker, which is the same population and excludes
|
|
71
|
+
a STEP's failing exit. Known limit: one spine is read, so a resume of a resume inherits the
|
|
72
|
+
previous leg's chain and not the one before it. `scripts/run-u.mjs --resume` wires it and names
|
|
73
|
+
the inherited baselines in its preview.
|
|
74
|
+
- **`--resume` on `scripts/run-u.mjs`** (PRD v1.46 §3) — the third leg of the resume rulings.
|
|
75
|
+
It skips the patient reset (`resumeTreeGate` instead: a dirty tree is what a resume expects;
|
|
76
|
+
only a moved HEAD stops it), folds the halted run's money and wall in so the signed ceiling
|
|
77
|
+
cannot widen by being re-invoked, re-enters at the checkpoint the steps already earned, and
|
|
78
|
+
arms the outside watchdog on the REMAINING wall. `readResume` gains two options, both OFF by
|
|
79
|
+
default so the reuse loop is byte-unchanged: `direct` reads a plain `runJob` spine as one
|
|
80
|
+
implicit try, and `resumableOutcomes` reclassifies a landed `job-end` that is a GOVERNANCE
|
|
81
|
+
HALT (`cap-halt`/`wall-halt`) as a checkpoint rather than a graded row. Green and every red
|
|
82
|
+
stay non-resumable — a verdict already rendered is never re-bought. `job-start` now carries
|
|
83
|
+
the declared fold (`priorSpentUsd` with its `priorSpendComplete`, and `priorWallMs`) when
|
|
84
|
+
there is one, so a chain of resumes adds only each attempt's new rounds instead of
|
|
85
|
+
re-deriving and double-billing.
|
|
86
|
+
**Validated live and pinned by cycle tests (F83).** Live: a signed top-up resumed a real
|
|
87
|
+
money cap-halt at its close checkpoint — finished step skipped, no re-scout or re-draft,
|
|
88
|
+
patient continued, watchdog armed on the REMAINDER — and greened, with `job-end` stating
|
|
89
|
+
the folded total across both legs. Pinned deterministically by five `$0` scripted-round
|
|
90
|
+
tests covering the whole cycle: leg-1 halt → top-up → leg-2 green at the checkpoint (a
|
|
91
|
+
close-precheck red proving the green was earned, not inherited); a **no-top-up control**
|
|
92
|
+
where the same budget buys no second pass and leaves a byte-identical tree (one
|
|
93
|
+
round-boundary overshoot round, the documented behaviour of a cap that binds BETWEEN
|
|
94
|
+
rounds); an **unsolvable cycle** where a topped-up leg making no per-stage progress
|
|
95
|
+
strikes out `flat` with the wallet still largely unburned — the fix loop's strike rule
|
|
96
|
+
governs a resumed leg exactly as it governs a cold one; and an F6 floor surviving halt →
|
|
97
|
+
fold → green terminal.
|
|
98
|
+
- **Layer 3, the REUSE rung — the registry, the load gate, and the MECHANICAL START**
|
|
99
|
+
(design record `docs/plans/2026-08-01-layer-3-reuse-design.md`; execution probe green
|
|
100
|
+
before any of it was built). A green's plan is now an artifact the next run of the same
|
|
101
|
+
SHAPE can start from, instead of being discarded.
|
|
102
|
+
- **`src/bridges.js` — the registry (D1/D6).** A directory of plain JSON files at an
|
|
103
|
+
OPERATOR-supplied path (no database, no new dependency, no default location).
|
|
104
|
+
`mintBridge` / `appendGreen` / `appendRed` / `saveBridge` / `loadBridge` /
|
|
105
|
+
`loadRegistry` / `makeRegistry` / `registryExists` / `validateBridge` / `listingRow` /
|
|
106
|
+
`deriveStatus`, all exported. **Only a green writes the box (R1):** a red writes a
|
|
107
|
+
history row and never touches `versions`. **Status is DERIVED, never stored (D6):**
|
|
108
|
+
candidate = one green, proven = greens on two DISTINCT patients, a red on a proven
|
|
109
|
+
entry demotes it — and a CASUALTY is not a red. There is deliberately no probability
|
|
110
|
+
score. Costs and durations are a number or an EXPLICIT `null` with the key required
|
|
111
|
+
either way, so an unknown is said rather than omitted (F6).
|
|
112
|
+
- **`loadGate(bridge, job)` — D2 as SPLIT (2026-08-01 addendum).** Three shape checks at
|
|
113
|
+
the door — verdict type, close-stage kinds, verbs within the signed menu — and
|
|
114
|
+
**nothing about paths, scopes or targets**: those are instance-bound and expected to
|
|
115
|
+
red, which the pre-probe measured the drafter fixing unaided (3/3 legal after tweak),
|
|
116
|
+
while the gate as originally frozen would have refused every cross-patient recipe.
|
|
117
|
+
- **`bridge?` on `runJob` and `runPlan` — the mechanical start (D4).** With a bridge, the
|
|
118
|
+
gate runs before the clock, the close precheck and any token; on a pass the NEWEST
|
|
119
|
+
version's plan-as-executed becomes the drafter's STARTING DRAFT inside the ordinary
|
|
120
|
+
drafting prompt (`planPrompt` gained an optional `startingDraft` argument). The tweaked
|
|
121
|
+
plan then passes the SAME `validatePlan` every cold draft passes — **no second, looser
|
|
122
|
+
path exists for an inherited plan** — and the ONE-replan ceiling is untouched (D5): a
|
|
123
|
+
replan drafts from the run's own state and never re-injects the bridge.
|
|
124
|
+
- **New terminal `recipe-stale`** (`runJob`/`runPlan`): a bridge that fails the load gate
|
|
125
|
+
is refused at the door with `bridge-gate {outcome, name, reds}` on the spine, a
|
|
126
|
+
decision-ready escalation, and **zero spend**. Falling back to cold drafting is the
|
|
127
|
+
CALLER's decision, never an automatic silent fallback. Classified as a deliberate
|
|
128
|
+
ledger exclusion — the gate refusing a wrong-KIND recipe is the mechanism working, not
|
|
129
|
+
a library bug to file upstream.
|
|
130
|
+
- **New spine events:** `bridge-gate` (a refusal, with its reds) and `bridge-loaded`
|
|
131
|
+
(`{name, versions, runid}` — which version inherited).
|
|
132
|
+
- **`src/selection.js` — D3's display half.** `renderListing(registry)` renders the
|
|
133
|
+
operator/LLM-facing workflow listing (name, goal, status, greens/reds, the cost/time
|
|
134
|
+
band of the greens), deterministic by name and total on garbage; an unknown cost or
|
|
135
|
+
time renders `UNKNOWN` and a partial aggregate says how many it skipped, and registry
|
|
136
|
+
entries that could not be read are NAMED so the listing is never quietly shorter than
|
|
137
|
+
the directory. `selectionPrompt(listing, ask)` is the pure prompt text for the pick,
|
|
138
|
+
carrying D3's two standing rules: *"none matches"* is a first-class answer that means
|
|
139
|
+
drafting new, and a PINNED workflow may be refused only EXPLICITLY, with a reason. Both
|
|
140
|
+
are pure text — the selection call, the pin/shortlist/force-cold flow and the parse of
|
|
141
|
+
the answer belong to the caller.
|
|
142
|
+
- **The cold path is byte-identical without a bridge** (the F47 works-both-ways rule),
|
|
143
|
+
pinned by test: no new event fires and no starting draft appears anywhere in the prompt.
|
|
144
|
+
- **`src/reuse.js` — the D7 ENVELOPE and the REUSE RUNNER.** hamr's sentence made
|
|
145
|
+
executable: *"we ask user for cost/time and how many workflows to try before starting
|
|
146
|
+
new like $5 and 30 mins x2 then start anew if both red"*.
|
|
147
|
+
- **`validateEnvelope(input, { job? })`** — `{ perTryBudgetUsd, perTryWallMs,
|
|
148
|
+
bridgeTries }`, **all three required and explicit, no defaults** (a defaulted cap is
|
|
149
|
+
a silent second ceiling); `bridgeTries: 0` = force-cold. The envelope may only
|
|
150
|
+
**TIGHTEN** the signed spec — a per-try number above `job.budgetUsd`/`job.maxWallMs`
|
|
151
|
+
is `envelope-widens`, never a silent raise.
|
|
152
|
+
- **`resolveTrySpec(job, envelope)`** — the per-try spec every try runs, cold leg
|
|
153
|
+
included. Tightening moves the SPEC HASH, so a tightened run is a new spec VERSION
|
|
154
|
+
the operator signs; `runReuse` passes `approvals` straight through and cannot forge
|
|
155
|
+
one. An envelope equal to the spec's numbers is hash-identical.
|
|
156
|
+
- **`selectBridge({registry, job, ask, provider, pinned?, shortlist?, forceCold?,
|
|
157
|
+
exclude?})`** — ONE model call on the caller's drafter-tier provider (none is
|
|
158
|
+
constructed inside), parsed through the repo's one `extractArtifact` into a strict
|
|
159
|
+
`{choice, reason}`. `forceCold` and an empty candidate set skip the call for $0. A
|
|
160
|
+
name outside the listing is a red, never used. **A pin does not bypass the call**
|
|
161
|
+
(D3): it is stated in the prompt, and an answer naming anything else returns
|
|
162
|
+
`{refused: true}` — the substitute is NOT adopted and the decision goes back to the
|
|
163
|
+
operator. Cost metered and reported (F6).
|
|
164
|
+
- **`runReuse({job, approvals, registryDir, envelope, patient, workdir, provider,
|
|
165
|
+
emit, …})`** — selection → try → box → next try → cold. Selection re-reads the
|
|
166
|
+
registry per try with the tried names excluded; **a green ENDS the loop**; tries
|
|
167
|
+
exhausted falls through to a cold run under the same per-try numbers, whose green
|
|
168
|
+
mints a new bridge for the job slug (or appends if that name already holds greens —
|
|
169
|
+
clobbering a green is what R1 exists to prevent), and whose red writes nothing (the
|
|
170
|
+
entry bar is a green). Every non-green return is decision-ready. The three answers
|
|
171
|
+
**no further try could change** — `unapproved-spec` (a tightened envelope is a new
|
|
172
|
+
spec version, signed not inherited; the decision hands over the exact hash),
|
|
173
|
+
`job-red`, `smoke-red` — end the run where they happen instead of reproducing
|
|
174
|
+
themselves down the whole envelope. A cold green also REFUSES to mint over a file of
|
|
175
|
+
that name it could not read (`mint-collision`): an unreadable entry is not an absent
|
|
176
|
+
one, and whatever greens it held are gone once overwritten.
|
|
177
|
+
- **`REUSE_GRADED_RED`** — only `escalated` demotes: the terminal where the close
|
|
178
|
+
judged the tree, the fix loop spent its attempts with money still on the table, and
|
|
179
|
+
the close was still red. Every other non-green outcome is a CASUALTY recorded under
|
|
180
|
+
its own name (`cap-halt`/`wall-halt` governance, `provider-red`/`step-stalled`
|
|
181
|
+
transport, `close-red` the close FAULTING and rendering no judgment, `recipe-stale`
|
|
182
|
+
refused at the door, `plan-red`/`step-red:*`/`check-red` stopping before the close
|
|
183
|
+
ever judged). Full detail still lands on the history row.
|
|
184
|
+
- **F45 made visible instead of thresholded.** A try's budget must fund the attempt
|
|
185
|
+
PLUS its close, and nothing in-process can know what a close costs — so **no
|
|
186
|
+
threshold is invented** (threshold-setting is arbiter territory, from a measured
|
|
187
|
+
base rate). Instead every try row carries `spentUsd` vs `capUsd`, `wallMs` vs
|
|
188
|
+
`wallCapMs`, `capBound`, `wallBound` and **`closeReached`**: a cap that died before
|
|
189
|
+
any grading is a fact on the row, not an inference.
|
|
190
|
+
- **New spine events:** `reuse-start`, `selection-result`, `try-start`, `try-end`,
|
|
191
|
+
`bridge-write`, `reuse-end`, plus `envelope-red`. **New escalation categories**
|
|
192
|
+
`reuse-exhausted` / `selection-refused` / `selection-red` / `registry-red` /
|
|
193
|
+
`envelope-red`, all added to the ledger's executable excluded-set — the envelope and
|
|
194
|
+
the operator speaking, never a library failing (a real transport fault out of the
|
|
195
|
+
selection call still classifies as `provider-red`).
|
|
196
|
+
- **`selectionPrompt(listing, ask, pinned?)`** gained the optional pin argument, so the
|
|
197
|
+
pin sentence has ONE spelling rather than being assembled by each caller.
|
|
198
|
+
- **`scripts/run-reuse.mjs`** — the reference operator runner: `--job` / `--registry` /
|
|
199
|
+
`--budget` / `--wall` / `--tries` (+ `--pin` / `--force-cold`), approval gate on the
|
|
200
|
+
RESOLVED per-try hash, seed refusal (it never resets the patient for you), F67 outside
|
|
201
|
+
watchdog sized for the SUM of the tries, gate-audit relocation, secrets scan, and a
|
|
202
|
+
sleep-inhibitor note in the header (F72 — a suspend freezes every guard, including the
|
|
203
|
+
watchdog).
|
|
204
|
+
- **RESUME AFTER A KILL (module C).** A killed reuse run comes back losing as little as
|
|
205
|
+
possible — hamr's ruling: *"money, signature and checkpoint (starts from where it
|
|
206
|
+
stopped) if mid loop, restart that loop"*.
|
|
207
|
+
- **`readResume(events, {deathAt?})`** turns a dead run's OWN spine back into the
|
|
208
|
+
state a resume continues from: which tries completed (never re-run, bridges still
|
|
209
|
+
spoken for), whether a try was mid-flight (a `try-start` with no `try-end` — never
|
|
210
|
+
graded, so never consumed), what that attempt already spent in money and wall, and
|
|
211
|
+
which signed envelope the spine belongs to. Spend is summed from `worker-round`
|
|
212
|
+
ONLY — the same event `runJob`'s ledger accounts — so a selection call's cost is
|
|
213
|
+
never attributed to a worker (F45); a restarted try DECLARES its inherited fold on
|
|
214
|
+
its own `try-start`, so a resume of a resume folds once rather than double-billing
|
|
215
|
+
an abandoned attempt. A try whose `job-end` landed is COMPLETE, not restarted, and
|
|
216
|
+
a registry row lost to the kill is named (`r1Missing`), never re-derived.
|
|
217
|
+
- **The remainder, never a fresh allotment.** `runJob` gained `priorSpentUsd` /
|
|
218
|
+
`priorSpendComplete` / `priorWallMs` and `createClock` gained `priorElapsedMs`: the
|
|
219
|
+
killed attempt's spend and time FOLD IN, so the restart runs under what is left of the
|
|
220
|
+
SIGNED per-try numbers. The caps themselves are never rewritten — they are in the spec
|
|
221
|
+
hash, and editing them would need a signature nobody typed. An unfundable remainder caps
|
|
222
|
+
honestly (`cap-halt`, or `wall-halt` below one close timeout) having launched
|
|
223
|
+
nothing. **A floor stays a floor across the resume:** if any round inside the dead
|
|
224
|
+
attempt came back unpriced, `priorSpendComplete: false` rides one-way onto every
|
|
225
|
+
`job-end` this run emits, so a resumed attempt states a floor instead of an
|
|
226
|
+
exact-looking total (F6 — it was previously computed and recorded on `try-start` but
|
|
227
|
+
never reached the component whose terminal says so). The restart fold reads ALL four
|
|
228
|
+
of `runJob`'s floor causes: a landed resumable `job-end` carrying
|
|
229
|
+
`spendComplete: false` (a stall, or a call cut mid-flight) floors the fold exactly
|
|
230
|
+
like a declared prior floor or an unpriced round (F83's laundering finding, fixed;
|
|
231
|
+
the no-`job-end` killed-mid-flight restart is control-pinned unchanged).
|
|
232
|
+
- **`runReuse({… resume})`** seeds the completed tries, restarts the mid-flight one
|
|
233
|
+
against the SAME workflow with no second selection call, and carries on into
|
|
234
|
+
whatever the envelope still authorizes. It refuses (`resume-red`, ledger-excluded)
|
|
235
|
+
a seed signed under a different envelope, or a restart whose workflow has left the
|
|
236
|
+
registry. New spine event `resume-start`; `try-start` gains the declared fold.
|
|
237
|
+
- **`resumeTreeGate({head, seed, dirty})`** — a resumed patient is CONTINUED, never
|
|
238
|
+
reset: the dead tries' edits are real progress. Only HEAD moving off the seed (a
|
|
239
|
+
commit or rebase under the dead run — operator intervention) stops it.
|
|
240
|
+
- **`makeSpine(file, {startSeq})`** so a resumed process continues one append-only
|
|
241
|
+
log with `seq` still monotonic, and **`scripts/run-reuse.mjs --resume <runid|path>`**:
|
|
242
|
+
it refuses a spine that is missing, corrupt mid-file, already terminal, or whose
|
|
243
|
+
process is STILL ALIVE (pid from the run's new `runner-start` record and the
|
|
244
|
+
watchdog report, discriminated by `/proc/<pid>/cmdline` because pids are recycled),
|
|
245
|
+
prints both hashes on an envelope mismatch, previews the whole reconstruction
|
|
246
|
+
before you sign, archives the killed run's watchdog record so it cannot be read as
|
|
247
|
+
this run's, and sizes the watchdog for the REMAINING work.
|
|
248
|
+
|
|
249
|
+
### Changed
|
|
250
|
+
- **`capRuns` retires as the CLOSE-FIX loop's governor** (PRD v1.46 §4). That loop now ends on
|
|
251
|
+
the same 2-strike no-progress rule the step ladder uses, read off the close's own per-stage
|
|
252
|
+
numbers rather than repeats and writes. Validated by a $0 replay over every archived fix
|
|
253
|
+
loop before the build: 0 greens harmed (all three historical fix-loop greens converted in
|
|
254
|
+
≤ 2 verdicts) and 1 real waste case caught (dead flat at 2 errors for 7 consecutive fix
|
|
255
|
+
verdicts until the wall killed it). `capRuns` survives ONLY as the bound for a close whose
|
|
256
|
+
output carries no number at all — a governor that cannot see the variable must not be the
|
|
257
|
+
governor — and lifts the moment a stage reports a comparable number. Money and the wall keep
|
|
258
|
+
every bit of their authority. The exhaustion terminal is unchanged in both category
|
|
259
|
+
(`cap-halt`) and outcome (`escalated`); `ralph`'s `ladder` option is now an INTERFACE two
|
|
260
|
+
governors implement, with an optional `terminal()` that overrides the exhaustion prose only
|
|
261
|
+
(the step ladder's copy offers a replan and there is no planner at the close). Every `ladder`
|
|
262
|
+
spine record now names its `governor` (`step-ladder` | `close-trend`). The wall-halt readout
|
|
263
|
+
no longer quotes `capRuns` as a denominator, because it no longer governs.
|
|
264
|
+
- **A plan step is bounded by PROGRESS, not by a count** (`src/ladder.js`; F77–F79). The step
|
|
265
|
+
loop's fixed iteration cap is replaced by a strike ladder: a *strike* is a red iteration that
|
|
266
|
+
repeats an already-seen gap (matched against a seen-set for the whole step, never just the
|
|
267
|
+
last one) or made no gate-audit writes (the F32 record-count instrument — never `git status`,
|
|
268
|
+
never a tree diff); strikes are sticky and `strikeLimit` of them (new shell option on
|
|
269
|
+
`runJob`/`runPlan`, **default 2**) ends the step. Gaps are normalized for comparison only —
|
|
270
|
+
timing bytes dropped, counts and file names kept — so a shrinking error count reads as
|
|
271
|
+
progress. The wallet, the wall, the variance meter and the stall fuse keep every bit of their
|
|
272
|
+
authority; the exhaustion terminal is the same `cap-halt`. Measured motivation: two
|
|
273
|
+
calibrations died `step-red` while still converging with money and wall unspent; a $0 replay
|
|
274
|
+
over 110 archived step ladders showed every converging red continuing and thrash ending
|
|
275
|
+
same-or-one-earlier. **`capRuns` now bounds the CLOSE-FIX loop only.** New per-iteration spine
|
|
276
|
+
record `ladder`; exhaustion records carry `strikes`/`strikeLimit`/`distinctGaps`.
|
|
277
|
+
- **The replan brief names the MECHANISM.** An exhausted step now tells the redrafting planner
|
|
278
|
+
which shape ended it (converging-cut / stalled-no-write / repeated-gap, with iteration numbers
|
|
279
|
+
as evidence), its gap trajectory, and how much money and wall the stop left unspent (time
|
|
280
|
+
reported UNBOUNDED when no wall was set, never `0`) — replacing *"it ran N attempts and its
|
|
281
|
+
exits were still red"*, which named no mechanism and read identically for opposite failures.
|
|
282
|
+
- **Layer R's VERBATIM ratchet stage is retired on the STEP path** (hamr's ruling): fixation is
|
|
283
|
+
a repeat, so a fixated step strikes out before the fourth iteration can open. The stage stays
|
|
284
|
+
reachable in the close-fix loop and fully unit-covered; Layer R still ships OFF by default.
|
|
285
|
+
|
|
286
|
+
### Deprecated
|
|
287
|
+
- **`steps[].attempts` is retired and INERT.** It is no longer offered in the drafting prompt
|
|
288
|
+
and the runner ignores it. It is still ACCEPTED by `validatePlan` — stored bridge plans carry
|
|
289
|
+
it, and a plan minted under one runner's number must not red under another's — with the shape
|
|
290
|
+
still checked and the old `<= capRuns` ceiling dropped. It was TIGHTEN-only, which made the
|
|
291
|
+
correct heal for a converging step inexpressible. `validatePlan`'s `capRuns?` option is
|
|
292
|
+
removed with the ceiling it fed (it existed only to bound `attempts`; an extra property is
|
|
293
|
+
ignored, so no direct caller breaks).
|
|
294
|
+
|
|
295
|
+
### Changed (the close-stage AXIS SPLIT — hamr: "#1 fix")
|
|
296
|
+
- **One POPULATION per close stage, so unlike numbers can never share a trend series.** The
|
|
297
|
+
trend reads one bucket per stage NAME, and a stage name is not an axis: `typecheck` red
|
|
298
|
+
either `N error(s) in <scope>` (the in-scope faults) or `the target files are clean but M
|
|
299
|
+
strict error(s) exist outside them` — a different population, reached only once the first is
|
|
300
|
+
zero — and `suite-green` mixed a failure count with an executed-count floor. A run crossing
|
|
301
|
+
either seam donated two genres to one series, so a 29 → 4 across it read **converging** and
|
|
302
|
+
would have recommended a top-up on work that had only swapped which wall it was behind.
|
|
303
|
+
Every mixed stage in the six `u-*` closes is now split: **`typecheck-outside`** carries the
|
|
304
|
+
outside-scope population immediately after `typecheck`, and **`tests-kept`** carries the
|
|
305
|
+
executed-count floor immediately before `suite-green`. Each new stage sits exactly where its
|
|
306
|
+
branch already ran, so the gate sequence is byte-order identical to the old single-stage
|
|
307
|
+
walk and first-red-wins is unchanged; `litectx`/`spawner` `typecheck` was verified
|
|
308
|
+
single-population and left alone. `src/trend.js`'s KNOWN LIMIT is rewritten as the historical
|
|
309
|
+
record plus **the law for close authors** — the remedy for a mixed stage is that close's stage
|
|
310
|
+
list, never a detector taught to tell two prose shapes apart (the F49 precedent).
|
|
311
|
+
- **All six `u-*` spec hashes flip**, by construction: a close's stage names are in the
|
|
312
|
+
signed spec, so the runner refuses every one of them until it is re-signed. That is
|
|
313
|
+
refuse-until-re-signed working, not a migration to smooth over.
|
|
314
|
+
- **Stored bridges whose `closeStageNames` predate the change refuse to load** — the load
|
|
315
|
+
gate's close-stage check is the doctrine that a changed close is a different KIND of job.
|
|
316
|
+
`aurora-u-spawner` and `litectx-u` are in that state now and are re-minted by a green under
|
|
317
|
+
the new close, never edited into agreement.
|
|
318
|
+
- **The F28 trim announcement is restored** where the split surfaced its loss: the
|
|
319
|
+
outside-scope branch pre-sliced its own echo, silently suppressing the "trimmed" notice a
|
|
320
|
+
gap bound must announce (a 341-line elide is visible again).
|
|
321
|
+
- **New `tests/close-stages.test.js` (20 tests)**, including a can-it-fail pre-flight against
|
|
322
|
+
the pre-split tree, spec↔script stage-list agreement over spawned real closes, and the
|
|
323
|
+
29 → 4 repro reading as two stages with a same-stage contrast proving the test can fail.
|
|
324
|
+
Live `$0` validation: pulselog green across all four split stages, baremobile
|
|
325
|
+
`typecheck-outside` RED at 381 reading correctly as its own series. **1007 tests.**
|
|
326
|
+
|
|
327
|
+
### Fixed (whole-branch review, 2026-08-05 — 4 opus finders over `main...layer-3-reuse`; F84)
|
|
328
|
+
|
|
329
|
+
*Every confirmed finding fixed, each verified against source before it was believed; no fix
|
|
330
|
+
forced, and three finder claims corrected in detail during the fixing.*
|
|
331
|
+
|
|
332
|
+
- **CRITICAL — the close trend's BLIND CAP was a one-way latch.** `capRuns` survives as the
|
|
333
|
+
fallback for a close whose output carries no comparable number, and it was armed by
|
|
334
|
+
"has this leg ever compared anything" — which a bare STAGE ADVANCE satisfies, on
|
|
335
|
+
essentially every run that does any work at all. One advance therefore disarmed the
|
|
336
|
+
fallback **permanently**: a later run of numberless reds (the shipped aurora TESTGEN
|
|
337
|
+
`verdict` stage is exactly that shape) accrued neither a strike nor a blind tick and was
|
|
338
|
+
bounded by money and the wall alone, where the retired count would have stopped it.
|
|
339
|
+
Reproduced by execution before the change — 27 iterations, no terminal. The cap now bounds
|
|
340
|
+
the **consecutive uncomparable streak**: it lifts the moment the instrument can read and
|
|
341
|
+
**re-arms the moment it cannot**, binding a recovered run never and a blind-from-birth run
|
|
342
|
+
on exactly the iteration it always did (pinned byte-stable by test, at every cap). The
|
|
343
|
+
`report()` `blind` flag follows the same question — a live streak IS blindness, so the
|
|
344
|
+
terminal no longer offers "0/2 strikes" as the reason a loop it did not strike out was
|
|
345
|
+
stopped.
|
|
346
|
+
- **`priorFloor` no longer requires money to have been spent.** `runJob` derived it as
|
|
347
|
+
`spentUsd > 0 && priorSpendComplete === false`, so a resumed leg inheriting a fold of **$0
|
|
348
|
+
that was not exact** — every round of the dead attempt unpriced — came back reading as an
|
|
349
|
+
exact zero. A floor OF zero is still a floor (F6). Read off the flag alone now, and the
|
|
350
|
+
onward `job-start` declaration is emitted for that case too (`spentUsd > 0 || priorFloor`),
|
|
351
|
+
which is the only field that can carry the unknown forward.
|
|
352
|
+
- **A reuse RESUME's halt readout now spans the chain.** `restart.grades` was computed by
|
|
353
|
+
`readResume`, declared on the record, and then handed to nobody: only `scripts/run-u.mjs`
|
|
354
|
+
passed `resumeGrades` on. `runReuse`'s own per-try execution now threads it into `runJob`,
|
|
355
|
+
so a restarted leg's `money-halt`/`wall-halt` trend judges the RUN rather than restarting at
|
|
356
|
+
this leg's first close and reporting `flat` — *revise the goal* — on a run that was
|
|
357
|
+
converging when its allowance ran out. `resume-start`'s `restart` states
|
|
358
|
+
`gradesInherited` (a COUNT; the seam carries stage names and numbers, never a close byte),
|
|
359
|
+
and only when there is one — the cold path is unchanged.
|
|
360
|
+
- **The `money-halt` record carries `spendComplete`.** `remainingUsd` is `budget − spent`, so
|
|
361
|
+
a run whose spend is a FLOOR has a remaining that is a CEILING; quoting it to four decimals
|
|
362
|
+
read as the number. `runPlan` takes the ledger's own `spendComplete` (one spelling of the
|
|
363
|
+
four causes, never a second arithmetic) and states it beside the figure — the same duty
|
|
364
|
+
`wall-halt` has carried since W-2.
|
|
365
|
+
- **A try row's ROUNDS fold across a resume, like its money and its wall.** A restarted try
|
|
366
|
+
reported only the restart's turns against a span its `$` already covered both legs of.
|
|
367
|
+
`try-start` declares `priorRounds` (including when it is `null`: absence means a spine older
|
|
368
|
+
than the field, an explicit null is a predecessor that could not count its own turns — and
|
|
369
|
+
an unfoldable count stays `null` rather than becoming the half this leg can see, F6). The
|
|
370
|
+
inherited-graded row's `wallMs` is likewise measured to the try's **own terminal** now, not
|
|
371
|
+
to a kill that may have landed minutes later.
|
|
372
|
+
- **The replan brief no longer calls an IDLE strike-out "converging".** The sentence was gated
|
|
373
|
+
on repeats alone, so a step that wrote nothing in any iteration was handed *"no file was
|
|
374
|
+
written in iteration(s) 1, 2"* and *"the step was converging"* in the same paragraph — to
|
|
375
|
+
the one channel the redrafting planner adapts to. Both signals gate it now; a moving exit
|
|
376
|
+
output on a step that wrote nothing is a close re-running, not work converging.
|
|
377
|
+
- **`planPrompt`'s JSDoc was detached from `planPrompt`.** An intervening helper landed between
|
|
378
|
+
the block and the function, so every parameter shipped as `any` in the generated `.d.ts`.
|
|
379
|
+
Reattached; no behaviour change, and the types are the contract.
|
|
380
|
+
- **W-2 on the LAUNCH side: a zero wall remainder now REFUSES, loudly.** `--wall-ms 0` reached
|
|
381
|
+
`scripts/u-watchdog.mjs`, which defaulted a non-positive value to `null` and armed **no
|
|
382
|
+
deadline at all** while the runner's own banner still advertised one — a guard reading zero
|
|
383
|
+
and saying nothing. The watchdog now refuses any PRESENT-but-unreadable numeric flag
|
|
384
|
+
(`0`, negative, garbage) with exit 2, while an OMITTED `--wall-ms` stays the legal unbounded
|
|
385
|
+
choice with the stale trigger armed. Both runners refuse above the spawn, before the key and
|
|
386
|
+
before the patient: `scripts/run-u.mjs` on an exhausted resume remainder, `scripts/run-reuse.mjs`
|
|
387
|
+
on `plannedWallMs <= 0`. **Two zeros, two levers** — a restart whose wall is burned wants
|
|
388
|
+
`--wall` (NO WALL LEFT), a run whose authorized attempts have all run wants `--tries` or
|
|
389
|
+
nothing (NOTHING TO ARM) — because naming the wrong lever sends the operator to re-sign a
|
|
390
|
+
number that buys no attempt. Both are in the approval hash, so either is a new signature.
|
|
391
|
+
- **`scripts/run-reuse.mjs` hardening.** A corrupt/truncated watchdog report can no longer abort
|
|
392
|
+
a signed resume at the last gate (it is parsed once, defensively, and reused; the archive
|
|
393
|
+
suffix falls back to the ARCHIVE moment so a second unreadable report cannot overwrite the
|
|
394
|
+
first). `runner-start`'s `argv` is redacted **at the write site** with the shipped
|
|
395
|
+
`redactSecrets` inventory — an append-only log that captures a key captures it forever, so an
|
|
396
|
+
after-the-fact whole-file scan is too late. A trailing value-flag (`--tries` with nothing
|
|
397
|
+
after it) now refuses instead of reading `Number('') === 0` and silently reshaping the run
|
|
398
|
+
being signed. The header's deliberate-differences list gains the fourth: the end-of-run leak
|
|
399
|
+
scan is a READOUT here, not a gate, because `runReuse` writes registry rows mid-run.
|
|
400
|
+
- **Archived runners annotated** (`reuse-preprobe`, `run-calibration-testgen`, `run-poc-layer2`,
|
|
401
|
+
`run-probe-testgen`): their `CAP_RUNS` comments described the retired drafting-prompt
|
|
402
|
+
interpolation, not the close-fix loop's blind fallback it actually is now.
|
|
403
|
+
|
|
404
|
+
- **Tests: 987 total** (from 959). The mutation-proven hole: a widening of `REUSE_GRADED_RED` — the
|
|
405
|
+
set whose members DEMOTE a proven bridge — survived the entire suite, so the demotion table
|
|
406
|
+
is now pinned outcome by outcome (`escalated` demotes; `step-red`/`cap-halt`/`wall-halt`/
|
|
407
|
+
`provider-red`/`close-red`/`step-stalled`/`pricing-red` are casualties and proven stays
|
|
408
|
+
proven). Plus the blind-cap backstop battery (blind-from-birth pin, streak-not-birth,
|
|
409
|
+
never-fires-while-reading, recovery resets), the launch-side refusals at all three sites,
|
|
410
|
+
the resume grade/round folds with their absent-baseline controls, the money-halt floor
|
|
411
|
+
readout, and a first battery for `scripts/migrate-bridge-patient-slugs.mjs` (dry-run
|
|
412
|
+
byte-identity, whole-file refusal over half-migration, name/filename disagreement — 4
|
|
413
|
+
mutants killed).
|
|
414
|
+
|
|
415
|
+
## [0.6.0] — 2026-07-31
|
|
416
|
+
|
|
417
|
+
### Added
|
|
418
|
+
- **P — the worker's palette widens 6 → 14 verbs, and a step gains a vocabulary** (design
|
|
419
|
+
record 2026-07-28). The plan surface exposed two of litectx's verbs and the agent could
|
|
420
|
+
choose almost nothing about HOW a step ran (F56).
|
|
421
|
+
- `TOOL_MENU` (`src/job.js`) is now four components: **write** (`write` · `edit`), **select**
|
|
422
|
+
(`read` · `grep` · `recall` · `get` · `impact` · `related` · `recent`), **compress**
|
|
423
|
+
(`compress` · `peek`), **isolate** (`stash` · `remember` · `forget`). Every verb maps to an
|
|
424
|
+
EXISTING litectx/bare-agent implementation — the menu is an INVENTORY, nothing was built to
|
|
425
|
+
fill it. Select/compress verbs are gate-judged as `read` actions through the same fence;
|
|
426
|
+
isolate verbs write the `.litectx` STORE and carry their OWN gate action types, so a store
|
|
427
|
+
park can never be counted as a tree write by the F32 `workerWrites` instrument. `run` stays
|
|
428
|
+
locked at every layer.
|
|
429
|
+
- New `WRITE_VERBS` / `STORE_VERBS` split: **neither class is read-capable**, so the scout's
|
|
430
|
+
grant filters both, and a ceiling made only of them reds `invalid-value` at sign time (a
|
|
431
|
+
write-only ceiling hands the scout an empty menu and it surveys blind).
|
|
432
|
+
- Each component ships its F19 strategy line (`COMPONENT_STRATEGIES`, `src/tools.js`),
|
|
433
|
+
composed into the persona only for components with a granted verb — capability without
|
|
434
|
+
strategy is inert, and a strategy for a verb the worker does not hold is noise.
|
|
435
|
+
- **Three new step fields, each legal set handed over as a MENU** (`src/plan.js`,
|
|
436
|
+
choose-don't-describe): `model` (`sonnet` | `haiku`, `STEP_MODELS` — `opus` deliberately
|
|
437
|
+
absent, hamr-assigned only, never plan-selectable), `attempts` (integer `1..capRuns`), and
|
|
438
|
+
`scope` (one value from the same `legalScopes` menu `tree-changed` uses). All optional, all
|
|
439
|
+
**tighten-only** — a plan may narrow the shell's caps and the signed fence, never widen them.
|
|
440
|
+
- **Each field is WIRED or REFUSED, never silently ignored** (the F50 blind-instrument class):
|
|
441
|
+
`scope` narrows the live gate fence (proven — an in-job-fence write denied by the step's own
|
|
442
|
+
scope), `attempts` tightens `ralph`'s cap (proven — exactly one iteration), and `model` routes
|
|
443
|
+
that step's worker through a new **`providerFor(tier)`** factory on `runJob`/`runPlan`.
|
|
444
|
+
**A step naming a tier when the caller supplied no factory is a NAMED `interpreter-red` stop**
|
|
445
|
+
— a wiring gap, not a plan defect — never a silent run on the default tier as if the choice
|
|
446
|
+
had been honoured.
|
|
447
|
+
- `scripts/run-u.mjs` supplies that factory (the tier→model mapping is the RUNNER's territory,
|
|
448
|
+
never the schema's) and gains a `--model` tier flag, and now **deletes `.litectx` at every
|
|
449
|
+
reset — cold means cold**: the isolate verbs persist across runs, so an uncleaned store would
|
|
450
|
+
leak run N's memory into run N+1's "cold" baseline and quietly poison the reuse rung's OFF arm.
|
|
451
|
+
- The U job specs widen to the full catalog, which is a NEW spec hash and therefore UNSIGNED
|
|
452
|
+
until hamr's explicit in-turn approval — widening the menu never silently changes a signed
|
|
453
|
+
spec's meaning.
|
|
454
|
+
- POC-first paid three times before a line of module code (recall hits carry no id — the path
|
|
455
|
+
IS the key; `impact`'s `confirmed`/`mentions` are counts, not arrays; `purge()` covers neither
|
|
456
|
+
stash nor memory, so a cold reset must delete the store). The tool tests run against a REAL
|
|
457
|
+
LiteCtx over a real fixture: the impact-shape test failed once against live data and was fixed
|
|
458
|
+
from the measured shape, not the assumed one.
|
|
459
|
+
- **F66 — the in-process stall fuse** (`src/stall.js`): bare-agent's `timeoutMs` bounds
|
|
460
|
+
socket INACTIVITY (resets on every byte), so a slow-but-trickling call hung a run for 274
|
|
461
|
+
minutes (U run ms3197n8). The fuse's heartbeat is a completed ROUND — no round for 5
|
|
462
|
+
minutes abandons the call and silently reissues it (self-heal, hamr's ruling); three
|
|
463
|
+
stalls throw `step-stalled`, the third replan trigger. Validated on the paid surface: a
|
|
464
|
+
real slow call stalled and its reissue answered; three real in-flight calls abandoned at
|
|
465
|
+
the ceiling; late answers from abandoned sockets swallowed. Mutation battery 6/6 (the
|
|
466
|
+
surviving mutant was a real hole matching the production failure shape — fixed).
|
|
467
|
+
- **F67 — the outside watchdog** (`scripts/u-watchdog.mjs`): U run ms3jh76q froze its event
|
|
468
|
+
loop 81.5 minutes and every in-process guard froze with it. A separate process sharing
|
|
469
|
+
nothing with the run — reads one file's mtime, calls kill(2), records the reason on disk
|
|
470
|
+
BEFORE the signal. Two triggers: stale spine and wall+grace. Battery includes a
|
|
471
|
+
hard-frozen-loop victim (`for(;;);`) — the exact case it exists for. Mutation 5/5.
|
|
472
|
+
- **Event-loop lag sampler** in `scripts/run-u.mjs`: records any ≥3s loop block with
|
|
473
|
+
from/until timestamps to `<spine>.lag.jsonl` — the instrument that localized the
|
|
474
|
+
spawnSync-close blocks (below) on its first run.
|
|
475
|
+
- **BREAKING for plan authors — `step-scope-escape`: a scoped step's TARGET must sit inside
|
|
476
|
+
its OWN narrowed scope** (`src/plan.js`, W3). A plan that validated before can red now. A
|
|
477
|
+
step may narrow the signed fence with `scope`, and `runPlan` builds that step's gate from
|
|
478
|
+
the narrowed prefix — so a target inside the signed fence but outside the step's own scope
|
|
479
|
+
was a write the gate denied on every attempt: the step burned its whole attempt budget on
|
|
480
|
+
refusals with no red anywhere, because each half was individually legal and nothing checked
|
|
481
|
+
the PAIR. The rule binds WRITES, not observations, and the exit arms were measured against
|
|
482
|
+
`src/exits.js` rather than assumed. `tree-changed` reds only when its scope is DISJOINT from
|
|
483
|
+
the step's — a scope that CONTAINS the step's contains its writable ground and fires exactly
|
|
484
|
+
when the narrow one does, and the menu is shallowest-first while the prompt says copy one
|
|
485
|
+
value, so containment would tax the likeliest draft (the F60 class `legalScopes` exists to
|
|
486
|
+
remove). `artifact-written`/`json-valid` paths carry NO step-scope red at all: the evaluator
|
|
487
|
+
reads and parses the named file and asks nothing about who wrote it, so a path naming a
|
|
488
|
+
PRIOR step's artifact is legal AND satisfiable under strictly-sequential v1 — a red there
|
|
489
|
+
would claim a constraint the evaluator does not impose. An off-menu `scope` carries no
|
|
490
|
+
narrowing (it already redded): one defect, one red.
|
|
491
|
+
- **Four documented names are now actually exported** (`src/index.js`): `runStages`,
|
|
492
|
+
`checkMenu`, `STORE_VERBS` and `stageClose`. Each was named as adopter-reachable in this
|
|
493
|
+
CHANGELOG or in `bareloop.context.md` while the index never handed it out — a
|
|
494
|
+
documented-but-unexported name is a false contract the adopter discovers by importing it.
|
|
495
|
+
`tests/index.test.js` pins the documented surface by name (a NAME check; each export's
|
|
496
|
+
semantics stay owned by its own module's tests), so the docs and the index cannot drift
|
|
497
|
+
apart silently again.
|
|
498
|
+
- **A per-payload cap on the isolate verbs** — `ISOLATE_MAX_BYTES` = 64KB, hamr's threshold
|
|
499
|
+
(`src/tools.js`). `ctx_stash`/`ctx_remember` write the `.litectx` store, which the
|
|
500
|
+
writeScope fence does not judge: the one worker surface that writes UNGATED bytes, and
|
|
501
|
+
unbounded it grows for as many rounds as the wallet funds. Over-cap comes back as a refusal
|
|
502
|
+
RESULT naming the actual size and the limit — worker feedback, never a throw (throws stay
|
|
503
|
+
the BA-4 param-guard class) — and nothing enters the store; both tool descriptions state
|
|
504
|
+
the bound. The residual is named rather than hidden: this bounds ONE PAYLOAD, and N
|
|
505
|
+
distinct ids are N payloads, so the aggregate stays unbounded. An aggregate cap is a
|
|
506
|
+
threshold, and thresholds are hamr's call.
|
|
507
|
+
- **`step-stalled` has its own DECISIONS entry** (`src/ralph.js`). F66's terminal fell through
|
|
508
|
+
to the generic default, which points a human at the interpreter — the one component that did
|
|
509
|
+
nothing wrong. A stall is not a fault: the socket stayed alive the whole time (that is the
|
|
510
|
+
measured mechanism), so the entry says so and leads with the planner's lever (replan) instead
|
|
511
|
+
of sending anyone to debug transport.
|
|
512
|
+
- **`ctx_impact` ships litectx's hedges** (`src/tools.js`), as `caveat` lines. litectx's impact
|
|
513
|
+
is deliberately asymmetric — over-count safe, under-count dangerous — so "isolated / low
|
|
514
|
+
risk" only ever ships HEDGED, and dropping the caveat left `risk low — 0 confirmed caller(s)`
|
|
515
|
+
reading as a licence to change the symbol freely, the one act the asymmetry exists to
|
|
516
|
+
prevent. The spine's `hits` field is counted BEFORE the caveats are appended and keeps its
|
|
517
|
+
old meaning (impact content) — a caveat qualifies the count, it is not more of it, and
|
|
518
|
+
re-meaning a shipped field is how a field stops measuring what its readers think it measures.
|
|
519
|
+
|
|
520
|
+
### Fixed
|
|
521
|
+
- **The close-fix loop laundered a transport casualty into a flat `escalated`**
|
|
522
|
+
(`src/planrun.js`). The step loop restores an escalation's own category (F11: the outcome and
|
|
523
|
+
the spine escalation must agree); the fix loop did not, so a `provider-red` raised there came
|
|
524
|
+
back as `escalated` — and `run.js` keys the F44 `spendComplete:false` floor on the OUTCOME, so
|
|
525
|
+
the run reported an exact-looking total for a call that may never have billed back. Now
|
|
526
|
+
mirrored. Found by the hardening pass, PARKED, and fixed only on hamr's explicit go.
|
|
527
|
+
- **The close-fix loop could not tell a drained wallet from spent attempts** (`src/planrun.js`,
|
|
528
|
+
F45 class). The shell spells BOTH terminals `cap-halt` — attempt-exhaustion and a money-gate
|
|
529
|
+
halt thrown mid-attempt — so the category alone cannot split them and only the wallet can.
|
|
530
|
+
The fix worker's gate is built with the wallet at its MOST drained (every step's spend is
|
|
531
|
+
behind it), which is precisely where a money cut masquerades as "the fix failed" and becomes
|
|
532
|
+
a false capability read. The fix loop now reads the wallet exactly as the step loop does: a
|
|
533
|
+
drained wallet returns `cap-halt` (the resume-to-cap checkpoint — the stop IS the
|
|
534
|
+
checkpoint), attempts spent with money still on the table stays the designed `escalated`
|
|
535
|
+
terminal ("the close is still red"). Reachability validated before the fix: nothing guards
|
|
536
|
+
money between the last step and the close. Verdict routing is arbiter territory — fixed only
|
|
537
|
+
on hamr's explicit go.
|
|
538
|
+
- **`ctx_recall` was blind to the memory axis, so `ctx_remember` was write-only through the tool
|
|
539
|
+
surface** (`src/tools.js`). The isolate strategy promises *"record a durable conclusion with
|
|
540
|
+
`ctx_remember` so a later step can `ctx_recall` it"* — and the recall handler hardcoded
|
|
541
|
+
`kind: 'code'`, so no note ever came back. The library-level round-trip test could not see it:
|
|
542
|
+
it called `lc.recall` directly, not the verb the worker holds. Recall now queries the fact axis
|
|
543
|
+
alongside code and returns each note on a line labeled `memory` with its BODY inline (capped at
|
|
544
|
+
400 chars, so a bloated note cannot page the context). The body rides because a note is a
|
|
545
|
+
CONCLUSION, not a pointer — there is no worker verb that dereferences a memory id. Same parked-
|
|
546
|
+
then-explicit-go path as above.
|
|
547
|
+
- **The aurora U close type-checked another repository** (`scripts/u-spawner-close.mjs`):
|
|
548
|
+
mypy's `explicit_package_bases` named the patient's files `packages.spawner.src.*`, so
|
|
549
|
+
sibling imports fell through to the editable install — a symlink to a DIFFERENT aurora
|
|
550
|
+
checkout. Proven with a planted probe, fixed with `MYPYPATH`; latent not active (seed
|
|
551
|
+
error count unchanged, both prior greens re-verified under the fixed instrument).
|
|
552
|
+
- **The stall fuse could be disarmed by the very call it abandoned** (`src/stall.js`, F70). An
|
|
553
|
+
abandoned call is not dead — it keeps streaming (ms3197n8's socket lived 274 minutes past
|
|
554
|
+
the fuse) — so a late beat from that corpse re-armed the watch timing its REPLACEMENT, and
|
|
555
|
+
the replacement could then hang forever without tripping the fuse built for exactly that.
|
|
556
|
+
One shared `Loop` compounded it, letting a corpse's round bound stop the live call. Now
|
|
557
|
+
generation-scoped: `watch`/`beat`/`isCurrent` all carry the generation of the call that
|
|
558
|
+
issued them, and each issued call gets its own `Loop`. Orphan rounds stay metered — an
|
|
559
|
+
abandoned call may already have been billed.
|
|
560
|
+
- **The outside watchdog could kill a stranger, or kill a live verdict**
|
|
561
|
+
(`scripts/u-watchdog.mjs`, `scripts/run-u.mjs`, F70). Liveness was `kill(pid, 0)`, which
|
|
562
|
+
answers "does SOME process hold this pid" — and after the SIGKILL/OOM case the watchdog
|
|
563
|
+
exists for, the kernel can recycle that pid onto an unrelated process. Liveness is now the
|
|
564
|
+
PARENT LINK (`process.ppid === pid`), which pid reuse cannot forge, and a guard aimed at a
|
|
565
|
+
non-parent REFUSES to arm at startup (exit 2, no marker) rather than running as a
|
|
566
|
+
plausible-looking guard pointed at nothing. Separately, its stale-spine window was sized
|
|
567
|
+
BELOW the longest legal silence — `runStages` emits nothing between stages, so a legal close
|
|
568
|
+
can be quiet for `closeTimeoutMs × stages` — and is now sized from the spec's own stage count
|
|
569
|
+
plus a margin. Test fixtures rewritten so the victim spawns its own watchdog: the only shape
|
|
570
|
+
that exercises the real parent link. **That closed the stale trigger's half of "kill a live
|
|
571
|
+
verdict" only:** the DEADLINE trigger still killed on the clock alone, on a wall grace whose
|
|
572
|
+
default covered one close stage, and hamr ruled the whole trigger must read activity before
|
|
573
|
+
it signals — the redesign is under *Changed*, below.
|
|
574
|
+
- **The ledger counted every stall as a bareloop bug, and then aimed it at the wrong package**
|
|
575
|
+
(`src/ledger.js`, F70 + review). `classifyIncidents` had no branch for `step-stalled`, so the
|
|
576
|
+
F66 fuse's own terminal fell through to "unclassified escalation category" — real upstream
|
|
577
|
+
evidence filed as a fake defect of ours. It was first routed through the TYPED-LIB branch
|
|
578
|
+
`interpreter-red` uses, and the rendered ask refuted that: `StallError` stamps
|
|
579
|
+
`lib: 'bare-agent'` at the throw site, so a live ask read *"bare-agent: the provider path
|
|
580
|
+
failed — worker stalled…"* when **nothing observed the provider path fail**. A stall is the
|
|
581
|
+
ABSENCE of beats. `step-stalled` is now a NAMED member of `EXCLUDED_ESCALATIONS` beside
|
|
582
|
+
`cap-halt` and `wall-halt` (operator ruling, on the wall-halt shape): our own governance
|
|
583
|
+
firing is not an upstream bug, and the stall evidence still rides the spine's own escalation
|
|
584
|
+
event, which is where a real ask would be sourced from anyway. Excluded by name, never
|
|
585
|
+
silently dropped — the excluded-set is executable and counted.
|
|
586
|
+
- **`checkMenu` expanded `needs` one level only** (`src/job.js`, F70), so a chain of
|
|
587
|
+
prerequisites ran incomplete — reproducing the false red `needs` exists to prevent, one level
|
|
588
|
+
deeper. Now a transitive fixed-point walk, ordered by close position, visiting each stage at
|
|
589
|
+
most once (cycle-safe by construction).
|
|
590
|
+
- **`ctx_impact` printed `calls undefined:undefined` for every callee** (`src/tools.js`, F70).
|
|
591
|
+
litectx returns `defs` and `callers` as objects with a path and a line, but `callees` as bare
|
|
592
|
+
NAMES; the readout assumed the object shape uniformly.
|
|
593
|
+
- **The aurora U close floored on tests PASSED, not EXECUTED** (`scripts/u-spawner-close.mjs`,
|
|
594
|
+
the F40 class recurring in a close written after the rule was minted). A skipped or deselected
|
|
595
|
+
test could hide a red without pushing the passed-count under the floor, and only one summary
|
|
596
|
+
bucket was read. Now floors on `executed = collected − (skipped + deselected)` and sums every
|
|
597
|
+
red tally; validated on the real patient (209 executed).
|
|
598
|
+
- `WRITE_VERBS` was declared twice — `src/plan.js` held its own frozen copy of `src/job.js`'s
|
|
599
|
+
array, so a third write-class verb could land in one and not the other. `plan.js` now
|
|
600
|
+
re-exports the one inventory (identity-tested, not deep-equal).
|
|
601
|
+
- **One defect, one red:** the mailbox rule fired on a step whose `tools` grant failed to
|
|
602
|
+
parse, deriving a second red from a false default on top of the correct
|
|
603
|
+
`missing-required`/`invalid-value` for the bad field. It now fires only on a PARSED grant —
|
|
604
|
+
the step's hands are unknowable otherwise, and the real defect already redded.
|
|
605
|
+
- **The retrieval verbs' line numbers are named as ONE 0-based handle space**
|
|
606
|
+
(`src/tools.js`). `recall`, `get` and `impact` all print the index's own 0-based positions —
|
|
607
|
+
one lower than the line an editor shows — and each now says so in its own tool contract. The
|
|
608
|
+
tempting alternative (render `impact` 1-based, since its numbers look display-only) was
|
|
609
|
+
tested against a real index and REFUTED: `impact`'s def range dereferences through `get`
|
|
610
|
+
verbatim and `recall` prints the same number for the same chunk, so renumbering one tool
|
|
611
|
+
would print two numbers for one chunk and turn `get`'s clean chunk-boundary refusal into a
|
|
612
|
+
guessing game.
|
|
613
|
+
- **The wall clock advertised a one-stage close** (`src/clock.js`, `src/planrun.js`, W5).
|
|
614
|
+
Enforcement is `maxWallMs + closeTimeoutMs` only when the close is ONE command; a staged
|
|
615
|
+
close (PRD v1.28) runs its stages one at a time and hands EACH the full `closeTimeoutMs`, so
|
|
616
|
+
a 4-stage close at 900s is 60 minutes of close, not 15 — and the run's own outside watchdog
|
|
617
|
+
already sized its stale window that way, which made it the same arithmetic disagreeing with
|
|
618
|
+
itself. `createClock` now takes `closeStages` (threaded from `stageClose`, the ONE staging
|
|
619
|
+
the runner executes — never a second count), `enforcedMs` is the one expression both the
|
|
620
|
+
advertised record and the worst case are computed from, and `report()` puts `closeStages` on
|
|
621
|
+
the `wall-clock` record: without it a reader seeing enforced 90min against a requested 30min
|
|
622
|
+
cannot tell one 60-minute timeout from four 15-minute stages, and an unexplained gap is
|
|
623
|
+
exactly the number a later reader rounds back down to the requested one. The `MIN_WALL_MS`
|
|
624
|
+
floor is deliberately left at one stage — a threshold is operator territory, parked in
|
|
625
|
+
PRD v1.39 rather than re-derived here.
|
|
626
|
+
- **A run that ABSORBED a stall reported its spend as exact** (`src/run.js`, W1). The terminal
|
|
627
|
+
`step-stalled` already carried `spendComplete: false`; the common case does not reach it —
|
|
628
|
+
the F66 fuse abandons the hung call and silently reissues it (self-heal), and an abandoned
|
|
629
|
+
call is not a call that was never billed. So a run that stalls and then ends green, escalated
|
|
630
|
+
or at the cap also holds a FLOOR, not a total. `runJob` now watches the `stall` event and the
|
|
631
|
+
flag is one-way: any absorbed stall makes `spendComplete: false` on the job-end, whatever the
|
|
632
|
+
outcome. The same honesty applies to the wall: a `wall-halt` that cut a call mid-flight
|
|
633
|
+
(`cutMidCall`) reports a floor, while a wall stop read BETWEEN iterations — where no call was
|
|
634
|
+
in flight — stays exact.
|
|
635
|
+
- **The ctx verbs' spine channel was unscrubbed** (`src/planrun.js`). Every field a `ctx-tool`
|
|
636
|
+
event carries is MODEL-CHOSEN text — a recall query, a stash id, a symbol, a path the worker
|
|
637
|
+
spelled — and the spine is append-only: a log that captures a key captures it forever. The
|
|
638
|
+
emitter is now scrubbed once at the WIRING, with the same ONE inventory (`SECRET_PATTERNS`)
|
|
639
|
+
the close output goes through, so a new ctx verb cannot forget it — seventeen scrubbed call
|
|
640
|
+
sites would have been seventeen chances to.
|
|
641
|
+
- **A failing `json-valid` exit could put file bytes on the spine** (`src/planrun.js`). An exit
|
|
642
|
+
detail is supposed to be names and counts, but `json-valid` embeds `JSON.parse`'s own message
|
|
643
|
+
and V8 quotes a window of the SOURCE inside it — a body of 20 characters or fewer is quoted
|
|
644
|
+
WHOLE, and the short-prefix secret shapes (`xoxb-…`) fit inside that window — so the detail
|
|
645
|
+
can carry file bytes the WORKER chose, into a log that is append-only forever. `runPlan` now
|
|
646
|
+
scrubs every detail once at the `judge()` boundary, with the same ONE inventory
|
|
647
|
+
(`SECRET_PATTERNS`) the close output and the ctx-verb channel ride through. The boundary is
|
|
648
|
+
the place, not the three consumers below it: the same details become the `exit-eval` record,
|
|
649
|
+
a fault's escalation `detail`, and the worker gap ralph puts on the spine as
|
|
650
|
+
`close-verdict.gap` — scrubbing at the source makes the leak inexpressible, scrubbing per
|
|
651
|
+
consumer makes it something the next consumer has to remember. `evalExits` itself is
|
|
652
|
+
untouched and does NOT scrub; it is a public export, so an adopter wiring its results into
|
|
653
|
+
their own log applies their own redaction.
|
|
654
|
+
- **The stall fuse could reissue a call past the run's wall** (`src/stall.js`, L4). The clock is
|
|
655
|
+
consulted where a ROUND completes and after a step returns; a stall completes no round and
|
|
656
|
+
returns from no step, so nothing read it between the wall passing and the next reissue — a
|
|
657
|
+
worker stalling at its wall could be reissued for up to `maxStalls × stallMs` of calls the run
|
|
658
|
+
had already declined to authorize. The watch now takes the wall as a callback and reads it
|
|
659
|
+
FIRST, before the decision to spend again: past the deadline it gives up as `wall-halt`
|
|
660
|
+
(never `step-stalled`, which is a replan trigger — there is nothing left to re-allocate, and
|
|
661
|
+
the only remedy is `maxWallMs`). Not a retreat from the self-heal ruling: self-heal is what a
|
|
662
|
+
run does with time left.
|
|
663
|
+
- **A close that refused SIGTERM hung the run forever** (`src/ralph.js`). On a fault
|
|
664
|
+
(`ENOBUFS`, timeout) the child is asked to leave, but SIGTERM is a REQUEST — a runner with
|
|
665
|
+
its own handler or a trapping wrapper can decline it — and there was no second deadline, so
|
|
666
|
+
no `close` event ever arrived and the run's only remaining stop was the out-of-process
|
|
667
|
+
watchdog. A 2s grace, then SIGKILL, which nothing can refuse; the timer is `unref`'d so it
|
|
668
|
+
can never hold the host loop open (F68's whole point) and cleared on `close` so it cannot
|
|
669
|
+
fire at a child that already left. Verdict semantics are untouched: the FIRST fault already
|
|
670
|
+
named the outcome and the kill only enforces it.
|
|
671
|
+
- **The ReDoS detector mis-parsed an EMPTY character class and passed the hazard through**
|
|
672
|
+
(`src/plan.js`, F49). The scan carried the POSIX rule *"a leading `]` is a literal member"* —
|
|
673
|
+
which is not JS: `[]` is the EMPTY class and `[^]` the any-char idiom, and both CLOSE at that
|
|
674
|
+
first `]`. Skipping it ran the scan past the class's real end and swallowed every quantifier
|
|
675
|
+
after it, so `x[^](a+)+$` read as SAFE while `RegExp.test` on it does not finish in 15s on a
|
|
676
|
+
31-char body — an agent-authored exit pattern that passed validation and then hangs the run's
|
|
677
|
+
event loop, which is the whole failure F49 exists to make inexpressible. A false NEGATIVE is
|
|
678
|
+
the dangerous direction, so the scan now follows JS. Four cases join `REDOS_BAD`
|
|
679
|
+
(`x[^](a+)+$`, `(([^]+))+`, `(([]a+))+`, `[^]x(a+)+`). **The monotonicity claim is stated
|
|
680
|
+
honestly rather than assumed:** this is a PARSE correction, not the add-rejections-only
|
|
681
|
+
tightening F49's rule contemplates, and it is not rejection-monotonic by construction.
|
|
682
|
+
Measured over 14,862 compilable generated patterns (2,369 previously rejected), exactly 2
|
|
683
|
+
rejections disappear — both pathological `[]`-bearing shapes whose old `true` came from the
|
|
684
|
+
very misparse being fixed, both verified to finish `RegExp.test` in ~0ms — and every entry of
|
|
685
|
+
the checked-in `REDOS_BAD`/`GOOD`/`OVERREJECTED` corpus is unchanged. The claim is "measured,
|
|
686
|
+
no hazard lost", never "monotonic by construction".
|
|
687
|
+
- **The isolate verbs wrote model-authored text into the tree unscrubbed** (`src/tools.js`,
|
|
688
|
+
`src/validate.js`). `ctx_stash`/`ctx_remember` park worker-chosen bytes in
|
|
689
|
+
`<workdir>/.litectx` — a file INSIDE the tree that outlives the step and reads back inline
|
|
690
|
+
through `ctx_recall` — which is the hard line's own sentence (*a record that captures a key
|
|
691
|
+
captures it forever*), one hop away from the spine it was already written for. Both the
|
|
692
|
+
PAYLOAD and the KEY now go through `redactSecrets`, a new export housed beside the inventory
|
|
693
|
+
in `src/validate.js` so detection (`scanSecrets`) and redaction read the ONE
|
|
694
|
+
`SECRET_PATTERNS` list and cannot drift apart. It is IMPORTED, not injected: an injected
|
|
695
|
+
redactor is one a future caller can forget to pass, and this leak has to be inexpressible,
|
|
696
|
+
not optional. Every id-taking verb (`ctx_peek`, `ctx_forget`) scrubs on LOOKUP with the same
|
|
697
|
+
transform, or a stash would be unreachable by the id the worker spelled; and the
|
|
698
|
+
`ISOLATE_MAX_BYTES` cap is measured on the SCRUBBED payload — the one that actually enters
|
|
699
|
+
the store — because a size read off the pre-redaction text reports bytes that never landed
|
|
700
|
+
(the F6 blind-instrument class). The plan flow's own `scrub` collapses onto the same helper.
|
|
701
|
+
- **The gate audit was the one persistent channel still logging worker-chosen text in
|
|
702
|
+
cleartext** (`src/planrun.js`). `gate-audit.jsonl` lives IN THE TREE and is append-only, and
|
|
703
|
+
the `Gate` was built without a `secrets` config — so it ran on bareguard's default-on
|
|
704
|
+
backstop, which covers `apiKey`/`authorization`/`Bearer …`/`sk-…` and nothing else. Measured:
|
|
705
|
+
a `ghp_` token landed in cleartext through every model-authored identifier an action carries
|
|
706
|
+
(an isolate verb's `id`, a path the worker spelled), and masked once the gate is handed
|
|
707
|
+
`SECRET_PATTERNS`. Content was never reachable here — `toolAction` reduces a write to
|
|
708
|
+
`{bytes}` — but the identifiers are raw model text. Third channel, same ONE inventory.
|
|
709
|
+
- **A plan `parse-error` could carry the DRAFT's bytes onto the spine** (`src/planrun.js`, the
|
|
710
|
+
`judge()` precedent one layer up). A `parse-error` red's detail is `JSON.parse`'s own message,
|
|
711
|
+
and V8 quotes a window of the SOURCE inside it — a body of 20 characters or fewer is quoted
|
|
712
|
+
WHOLE, which is exactly the short-prefix secret shapes. That source is the model's draft, so
|
|
713
|
+
the red rode onto `plan-validate`, onto `plan-red`, and back into the redraft prompt. Every
|
|
714
|
+
red's detail is now scrubbed ONCE at the validation boundary, before any consumer reads it —
|
|
715
|
+
the boundary, not the three emit sites, because scrubbing here makes the leak inexpressible
|
|
716
|
+
while scrubbing per consumer makes it something the next consumer has to remember.
|
|
717
|
+
- **A strategy line could name a tool the worker was never granted** (`src/tools.js`,
|
|
718
|
+
`src/planrun.js`) — F19 inverted, and worse than silence: the model spends rounds reaching
|
|
719
|
+
for a menu entry that does not exist. The strategy prose was gated per COMPONENT, so any one
|
|
720
|
+
isolate verb lit a paragraph prescribing `ctx_peek` (a COMPRESS verb), either retrieval verb
|
|
721
|
+
lit the two-step `ctx_recall` → `ctx_get` recipe, and any one select verb named all three.
|
|
722
|
+
New `strategyFor(granted)` assembles the same prose SENTENCE BY SENTENCE, each clause gated
|
|
723
|
+
on its own verb, with the select paragraph's `a, b, and c` grammar preserved. The full-menu
|
|
724
|
+
text is unchanged BY TEST, not by comment: `strategyFor(TOOL_MENU)` is asserted
|
|
725
|
+
byte-identical to the concatenated paragraphs, so a partial grant can only ever see LESS,
|
|
726
|
+
never different.
|
|
727
|
+
- **The eight palette verbs had no failure path** (`src/tools.js`). `ctx_get` has always
|
|
728
|
+
wrapped its own failures into a refusal RESULT and emitted them; the eight verbs P added
|
|
729
|
+
(`impact`, `related`, `recent`, `compress`, `stash`, `peek`, `remember`, `forget`) threw
|
|
730
|
+
straight out of the tool loop instead — and a thrown call is the most invisible result there
|
|
731
|
+
is, indistinguishable from a verb that was never reached, so the worker's fall-back to a
|
|
732
|
+
whole-file read reads as a free choice instead of a forced one (the F18 blindness rule). A
|
|
733
|
+
shared `guarded()` wrapper now hands the worker a refusal naming the real cause (a locked
|
|
734
|
+
store, a corrupt index) and emits `outcome:'error', bytes:0` to the spine — one round instead
|
|
735
|
+
of the run, and never a rethrow (L1: throws stay reserved for the BA-4 param-guard class).
|
|
736
|
+
Its field reader is deliberately defensive, since a field reader that throws would take the
|
|
737
|
+
emit down with it — the hole this closes.
|
|
738
|
+
- **`validatePlan` could THROW on plain parsed data** (`src/plan.js`). Its contract is that
|
|
739
|
+
every failure is a named red; `Array.isArray(job.writeScope)` admitted the array and asked
|
|
740
|
+
nothing about its MEMBERS, so the next line's `globToPrefix` hit `scope.replace is not a
|
|
741
|
+
function` and a `TypeError` escaped the validator. The fail-closed guard now asks the same
|
|
742
|
+
`isNonEmptyString` question of each member that `legalScopes` already asks of the same field.
|
|
743
|
+
- **Three enforced rules the drafter prompt never stated** (`src/planrun.js`). A bound the
|
|
744
|
+
validator reds but the prompt omits is a round burnt on a rule the drafter had no way to
|
|
745
|
+
know: `attempts` said "integer" while `validatePlan` reds outside `1..capRuns`, and BOTH
|
|
746
|
+
`step-scope-escape` reds (a `target` outside its own step's narrowed scope; a `tree-changed`
|
|
747
|
+
scope disjoint from it) were enforced silently. `planPrompt` now takes `capRuns` from the
|
|
748
|
+
SAME source the validator bounds against — one number, so prompt and validator cannot drift
|
|
749
|
+
— and states both scope rules in its own voice.
|
|
750
|
+
- **The `check-passes` exit example was the one malformed line among four** (`src/planrun.js`).
|
|
751
|
+
It carried the enumerated check menu INSIDE its JSON `"name"` string, so the example the
|
|
752
|
+
drafter is most likely to copy did not parse. The menu now sits BESIDE the example as its own
|
|
753
|
+
copy-it-character-for-character line, and the regression test parses EVERY exit example, one
|
|
754
|
+
per `EXIT_TYPES` entry, rather than the single `tree-changed` line it happened to pick.
|
|
755
|
+
- **The U close scripts' suppression rules missed two escapes that the paid runs actually
|
|
756
|
+
emit, and mislabelled a broken instrument as a timeout** (`scripts/u-litectx-close.mjs`,
|
|
757
|
+
`scripts/u-spawner-close.mjs`).
|
|
758
|
+
- The JSDoc WILDCARD (`{*}`, `{?}`) is `any` under a spelling the `any` rule cannot see — it
|
|
759
|
+
contains no letters. Measured against this repo's own `tsc` (`--noEmit --strict --allowJs
|
|
760
|
+
--checkJs`): a naked `function f(v)` reports TS7006, while `@param {*}`, `@param { * }`,
|
|
761
|
+
`@param {?}` and a `@typedef {*} Loose` used from a `@param {Loose}` each report ZERO
|
|
762
|
+
errors. So the new `any-star` rule matches the tag GENERICALLY (`@\w+`) rather than from a
|
|
763
|
+
list of tags — a list of tags is a list of holes. `{*[]}` / `{Array<*>}` are deliberately
|
|
764
|
+
NOT matched: measured, they still error on non-array use, so they are `any[]`, which the
|
|
765
|
+
`any` rule already covers.
|
|
766
|
+
- The CAST rule read its type body with `[^}]*`, which an inner brace closes early — so
|
|
767
|
+
`/** @type {{ a: number }[]} */ (expr)` walked straight through. That shape is not
|
|
768
|
+
hypothetical: it appears in this repo's own paid U-run diffs (`run3-ms3wawub`,
|
|
769
|
+
`run-ms5uxhej`). Now `.*`, which is bounded by construction — every rule is tested against
|
|
770
|
+
ONE diff line at a time.
|
|
771
|
+
- A `spawnSync` that produced NO exit code was reported as *"timed out"* whatever killed it.
|
|
772
|
+
Measured shapes (node 22): a timeout is `{status:null, signal:'SIGTERM',
|
|
773
|
+
error.code:'ETIMEDOUT'}`, a `maxBuffer` overflow is the same with `ENOBUFS`, an external
|
|
774
|
+
kill is `{status:null, signal:'SIGKILL'}` with no error at all. Collapsing them hands the
|
|
775
|
+
operator a wrong cause and hides a broken instrument behind a bound that never fired (the
|
|
776
|
+
casualty-vs-evidence class); each now names itself. The spawner close also gains the
|
|
777
|
+
sibling's `maxBuffer` (1 << 28) — a red pytest run prints every traceback, and the overflow
|
|
778
|
+
does not truncate, it SIGTERMs the child, i.e. a real red arriving dressed as an instrument
|
|
779
|
+
stop.
|
|
780
|
+
- **A close child ending in `process.exit()` drops its own queued stdout, and the runner read
|
|
781
|
+
the short capture as a short close** (F71, `tests/staged-run.test.js`,
|
|
782
|
+
`tests/ralph.test.js`). One test's staged-close fixture arrived truncated about 1 run in 250
|
|
783
|
+
under load — stream ending mid-line, the `FAILED:` names gone, exit code still 1 — which is
|
|
784
|
+
the F28 failure-names-lost class arriving nondeterministically. Root cause is the CHILD's:
|
|
785
|
+
node discards whatever is still queued on its stdout pipe when the process calls
|
|
786
|
+
`process.exit()`, and the parent then sees a clean EOF, a real exit code and no fault, so a
|
|
787
|
+
lost tail is indistinguishable from a close that simply printed less. Measured 7/500 shorts
|
|
788
|
+
with `process.exit(1)` against 0/500 with `process.exitCode = 1` on the identical fixture, and
|
|
789
|
+
12/12 deterministically with a 50ms reader stall. The earlier reading in that test's comment —
|
|
790
|
+
*"pipe writes are synchronous on Linux, so there is nothing pending to drop"*, and *"only the
|
|
791
|
+
runClose path loses bytes"* — is REFUTED on both halves and rewritten with the measurements
|
|
792
|
+
that refuted it. What shipped is the fixture's `process.exitCode` spelling (now named as the
|
|
793
|
+
fix it is, not a free tidy), a precondition asserting the stage's LAST line reached the gap
|
|
794
|
+
so a short arrival can never again surface as a confusing `gapKeep` failure three assertions
|
|
795
|
+
later, and a whole-capture guard pinning the half the RUNNER owns: past the pipe's 64KB
|
|
796
|
+
capacity, with the host loop frozen mid-stream, a close that exits cleanly is captured WHOLE
|
|
797
|
+
(mutation-checked — it kills a reader that caps accumulation at 64KB, 4/4). The residual is
|
|
798
|
+
named, not hidden: the production close scripts still end in `process.exit()`, safe today
|
|
799
|
+
only because they self-cap their output at 40 lines, which is inside the measured-immune
|
|
800
|
+
band. See F71.
|
|
801
|
+
|
|
802
|
+
### Changed
|
|
803
|
+
- **BREAKING for adopters — `runClose` and `runStages` are async and return Promises**
|
|
804
|
+
(`src/ralph.js`). The close child is awaited instead of `spawnSync`, so a running close no
|
|
805
|
+
longer freezes the host event loop (F68 — measured: a 3-stage close went from 0 host ticks
|
|
806
|
+
during 3.5s of close to 126 ticks with a worst gap of 26ms). Every close semantic is
|
|
807
|
+
byte-identical, verified by a 25-case old-vs-new differential: timeout signal (SIGTERM →
|
|
808
|
+
`timed-out`), 1 MiB per-stream output ceiling (`ENOBUFS` → broken-close, deliberately not
|
|
809
|
+
widened — an over-long close must not be judged on truncated output), gap shape/bounds,
|
|
810
|
+
exit bands, redaction, cwd, stdin EOF. The one visible difference: a spawn fault's detail
|
|
811
|
+
prose says `spawn` instead of `spawnSync` (nothing parses it).
|
|
812
|
+
- **BREAKING for plan authors — `check-passes` now requires a write-class verb (`write`/`edit`)
|
|
813
|
+
on the SAME step** (`src/plan.js`, `exit-illegal`). A shape that validated before is rejected
|
|
814
|
+
now: a read-only "verify" step that carries a check. Measured before it was built (P-read runs
|
|
815
|
+
ms4l5p6w / ms57zr7c): 4 of 4 drafted plans put every `check-passes` on a read-only verify step,
|
|
816
|
+
and both runs stalled to cap on a byte-identical gap delivered to a worker that could not edit.
|
|
817
|
+
A failing check's gap is re-delivered to THAT step's own worker, so the step must be able to act
|
|
818
|
+
on it — a read-only one is a mailbox with no hands. The check belongs on the step that fixes;
|
|
819
|
+
the run's final verification is the operator's close, which the agent never authors. The drafter
|
|
820
|
+
prompt states the same law, so prompt and validator can never disagree. The red is SUPPRESSED
|
|
821
|
+
when `tools` failed to parse: the step's hands are unknowable then, and charging the ledger with
|
|
822
|
+
a violation derived from a false default would double-red one defect.
|
|
823
|
+
- **BREAKING for an adopter holding an approval for an omitted-`tools` spec — the spec hash is
|
|
824
|
+
taken over the RESOLVED spec** (`src/job.js`, MED-1). `tools` is optional and its absence
|
|
825
|
+
means the whole `TOOL_MENU`; `plan.js` and `planrun.js` resolve it with exactly that
|
|
826
|
+
predicate at runtime, so an omitted-`tools` spec's ceiling GREW every time the menu did while
|
|
827
|
+
its bytes never moved — and that widening was unsigned. `jobSpecHash` and `checkApproval` now
|
|
828
|
+
canonicalize the same resolved form, with an omitted `tools` filled in as the concrete current
|
|
829
|
+
menu, so a signature pins WHICH menu it covers and a menu change flips the hash straight into
|
|
830
|
+
the existing refuse-until-reapproved machinery. A spec that named its own ceiling hashes
|
|
831
|
+
byte-for-byte as before — no migration for a signature already in flight — and an approval
|
|
832
|
+
minted for an omitted-`tools` spec re-signs once. Writing today's full menu into a spec that
|
|
833
|
+
omitted `tools` is hash-NEUTRAL, which is the point: the two spell the same ceiling. The
|
|
834
|
+
resolve sits INSIDE each path's existing throw guard, because a spread runs getters.
|
|
835
|
+
- **The wall PAUSES the run; it never cuts the grade** (`src/planrun.js`, `src/ralph.js`, W-2 —
|
|
836
|
+
hamr: *"when time is up, keep the grade we already have and stop"*). The close is never
|
|
837
|
+
bounded by the wall and always runs to completion: a deadline that kills grading leaves the
|
|
838
|
+
run unreadable after the money is already spent (the F45 class, generalized from money to
|
|
839
|
+
time). What the wall stops is the START of new work — past the deadline the close-fix loop
|
|
840
|
+
opens no further iteration, and the same rule covers a step that would begin already expired.
|
|
841
|
+
The run then stops on the verdict the last close minted, and nothing after the deadline is
|
|
842
|
+
allowed to change it. The iteration it declines was guaranteed worthless anyway: the metered
|
|
843
|
+
round check stops the fix worker on its FIRST round so it writes nothing, and the iteration
|
|
844
|
+
then re-runs the FULL staged close over an unchanged tree to mint the verdict already on the
|
|
845
|
+
record — `capRuns` times over. The stop is a decision-ready `wall-halt` carrying the verdict
|
|
846
|
+
that stands, how much of the loop was actually spent, and a progress TREND read off the two
|
|
847
|
+
most recent close gaps: byte-identical is `stalled`, different is `moving`, and one grade
|
|
848
|
+
alone is `unknown` — never rounded up to `stalled`, which would recommend rewriting a goal on
|
|
849
|
+
no evidence (F6's rule applied to a trend). The trend exists because the two levers point
|
|
850
|
+
opposite ways, and both are spec edits whose new hash needs re-approval: raise `maxWallMs`
|
|
851
|
+
and resume (resume-to-cap — the stop IS the checkpoint) or revise the goal so the work fits
|
|
852
|
+
the time; abandon is the third. `cutMidCall` still splits the two readings, so which
|
|
853
|
+
instrument saw the deadline stays on the record, and the step-site record names its two
|
|
854
|
+
caps SEPARATELY: `capRuns` is the RUN's attempt cap — what the fix-site record has always
|
|
855
|
+
written — and `stepAttemptCap` is the step's own tightened cap, the number that actually
|
|
856
|
+
bounded THIS step. Previously the one key carried both meanings depending on which site
|
|
857
|
+
emitted it, leaving a reader of two records of the same type no way to tell which. It is
|
|
858
|
+
not a universal bound and is not claimed as one: a STEP already under way still runs its
|
|
859
|
+
own exit checks after the deadline.
|
|
860
|
+
- **The close child no longer inherits the operator's credentials** (`src/ralph.js`,
|
|
861
|
+
`CLOSE_ENV_DENY`, PRD v1.41 — hamr: *"strip it when unnecessary, minimize unneeded
|
|
862
|
+
exposure"*). `runClose` built the child's env as `{...process.env}` minus
|
|
863
|
+
`NODE_TEST_CONTEXT`, and that child is not an ordinary subprocess: `npm test`, `pytest`,
|
|
864
|
+
`tsc` execute **worker-authored code**, with network, in the tree they are about to judge.
|
|
865
|
+
Nothing that judges a tree needs a provider key, so the exposure bought nothing. Two rules,
|
|
866
|
+
deliberately both: the KNOWNS (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`,
|
|
867
|
+
`GOOGLE_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, `NPM_TOKEN`, the whole
|
|
868
|
+
`AWS_*` prefix) exist only for names the shape rule cannot see, and the NAME-SHAPE rule —
|
|
869
|
+
anything ending `_API_KEY`/`_SECRET`/`_TOKEN`/`_PASSWORD`/`_CREDENTIAL(S)`, case-insensitive
|
|
870
|
+
— is the load-bearing one, because a literal denylist is a chase every new provider extends.
|
|
871
|
+
A COPY is stripped, never `process.env`: the host keeps the key bare-agent spends every
|
|
872
|
+
round. Placed at `runClose`, the single close-spawn seam, so `runStages` and every derived
|
|
873
|
+
`check-passes` inherit the same blinding — one instrument, never two. **Stated as what it is:
|
|
874
|
+
exposure reduction, NOT a sandbox.** bareloop runs locally as the operator's OS user, so
|
|
875
|
+
worker-authored code still has that user's file access (including the shell profile that
|
|
876
|
+
exports the variables just stripped) and the network. Real containment is a different
|
|
877
|
+
mechanism, unbuilt, and claimed nowhere; README and `bareloop.context.md` carry the same
|
|
878
|
+
boundary note. Arbiter territory: no drafted plan or config can express, widen, or opt out.
|
|
879
|
+
- **Scripts — unshipped, but the surface a user actually runs.**
|
|
880
|
+
- The outside watchdog's DEADLINE kill is ACTIVITY-AWARE (hamr: *"the kill from outside should
|
|
881
|
+
check for activity/bytes or other markers for activity, not a silent kill"*). Past the
|
|
882
|
+
deadline it kills only when the spine has ALSO been flat for a full stall-fuse window;
|
|
883
|
+
past-deadline-but-still-writing logs LOUD every poll and is never killed — the in-process
|
|
884
|
+
fuses and the money cap own bounding a run that is alive, this guard owns processes that are
|
|
885
|
+
dead. Every kill states the trigger, the deadline arithmetic and the marker's value and age
|
|
886
|
+
before the signal goes out. ONE marker, spine bytes: a CPU marker (`/proc` utime+stime) was
|
|
887
|
+
built alongside it and REMOVED after measuring broken in BOTH directions — a close runs its
|
|
888
|
+
suite in a CHILD whose ticks are credited only at reap, so a live close read DEAD; and
|
|
889
|
+
`run-u`'s own 1s lag sampler wakes the watchdog, so a wedged run read ALIVE at production
|
|
890
|
+
constants (its kill tests passed only at a dead-window:poll ratio of 5:1 where production
|
|
891
|
+
runs 60:1). hamr: *"keep what is simpler/available"*.
|
|
892
|
+
- The wall grace is `stages × closeTimeoutMs`, and `run-u` passes it. The watchdog's own
|
|
893
|
+
default is ONE stage, so a 4-stage close left on the default put the outside deadline at
|
|
894
|
+
wall+15min while a legal close can still be mid-verdict at wall+60min. A spec that sets no
|
|
895
|
+
`maxWallMs` now OMITS `--wall-ms` and names the unbounded choice out loud on both sides of
|
|
896
|
+
the process boundary, instead of passing the string `undefined` — which fell back to the
|
|
897
|
+
null default and silently disarmed the deadline trigger while the header still claimed a
|
|
898
|
+
wall. Same NaN-disarms-a-guard class, one flag over.
|
|
899
|
+
- `run-u`'s spine leak check is a TRIPWIRE, not a note in the margin: on any hit the run exits
|
|
900
|
+
**3** (distinct from 2 = operator/config and 1 = stale `--approve`) and the BRIDGE is NOT
|
|
901
|
+
written — a spine carrying a secret must never graduate into a reusable artifact that
|
|
902
|
+
outlives the run. Count and path only, never the matched content: echoing it to stdout is
|
|
903
|
+
the same leak, one hop on.
|
|
904
|
+
- `run-u` can no longer be killed by its own guard's startup. The watchdog path was taken
|
|
905
|
+
from `new URL(...).pathname`, which stays percent-ENCODED — a checkout under a directory
|
|
906
|
+
with a space hands `spawn` a `%20` path that does not exist, and the guard dies at second
|
|
907
|
+
zero on the one run it exists to protect (`fileURLToPath` now, the spelling the sibling
|
|
908
|
+
scripts already use). And a spawn failure arrives as an EVENT: an unhandled `'error'` on a
|
|
909
|
+
ChildProcess is an uncaught exception, so a guard that could not start would have ENDED the
|
|
910
|
+
paid run. It is handled LOUDLY instead — the run continues UNGUARDED from outside, under
|
|
911
|
+
its own fuses, wall and money cap, and says so, because a reader must never mistake a
|
|
912
|
+
missing watchdog marker for "the guard was watching and never fired".
|
|
913
|
+
- **All nine remaining hand-rolled secret scans in `scripts/` now call the canonical
|
|
914
|
+
`scanSecrets`** (11 call sites, 2 already canonical). F40 parked the conversion; hamr
|
|
915
|
+
retired the park on his word, and both risks it guarded were closed by MEASUREMENT rather
|
|
916
|
+
than assumption — equivalence on 900 spiked real-spine cases (670 hits, 0 divergences,
|
|
917
|
+
with the comparison demonstrated ABLE to diverge), and archived evidence that can no longer
|
|
918
|
+
shift. FINDINGS addendum sits on the F40 block; the JSONL spine-reader park is untouched.
|
|
919
|
+
- **litectx ^0.29.1 → ^0.31.0** (LC-3 delivered, yield-only shape — atomicity intact,
|
|
920
|
+
`setImmediate` between parses). Every worker's index call now passes `{ yield: true }`, so the
|
|
921
|
+
F66 stall fuse's timers, the wall clock and the lag sampler stay alive through an index pass
|
|
922
|
+
instead of dying inside one long block. Consumed on the BINDING max-block clause (189–252ms,
|
|
923
|
+
n=4, 2× headroom) **with the miss on record**: liveness read 54–58% on our fixture against a
|
|
924
|
+
60% proxy bar, and the criterion was NOT re-amended after measurement.
|
|
925
|
+
- bare-agent 0.34.0 → 0.35.0 (BA-19 delivered: `deadlineMs` total-call bound, `EDEADLINE`,
|
|
926
|
+
`context.bound` discriminator, terminal by design; bareloop wiring PARKED — the F66 fuse
|
|
927
|
+
already self-heals this class above the transport).
|
|
928
|
+
|
|
929
|
+
### Removed
|
|
930
|
+
- **BREAKING — the legacy operator-authored `steps[]` path is deleted** (PRD v1.32, hamr:
|
|
931
|
+
*"i just want to get rid of it"*). `steps[]` is the shape where the OPERATOR hand-writes the
|
|
932
|
+
workflow steps — the exact thing bareloop exists to remove (*"there shouldn't be user
|
|
933
|
+
authoring anywhere"*, v1.28) — and it had been co-existing scaffolding since Layer 2 landed,
|
|
934
|
+
forcing every downstream schema decision to carry two spellings.
|
|
935
|
+
- **Gone from the public surface:** `interpret`, `STALL_REDS`, `validateConfig`, `diffPaths`,
|
|
936
|
+
`extractRules`, `MAX_RULES`, `MAX_RULE_CHARS`, `LOOP_SHAPES`, `SLOTS`, `VERBS`, `CLASSES`,
|
|
937
|
+
`STEP_MODES`, and `runJob`'s `opts.target` / `opts.execCmd`. **config-v1 is fully removed**,
|
|
938
|
+
not merely declared dead (F22 declared it): the drafted-workflow-config schema, its knobs,
|
|
939
|
+
the drafting call, the rules extractor, and the hitl draft-PR mechanics went with the path
|
|
940
|
+
that consumed them. A spec still carrying `steps[]` reds `shape-retired:steps` **by name**,
|
|
941
|
+
so an old spec is told what happened instead of half-running.
|
|
942
|
+
- **What it costs, stated plainly:** `hitl` is not dead code — `jobs/litectx-maintainer.json`
|
|
943
|
+
ran 6 times and opened a real draft PR once, and it is the only job that produced the
|
|
944
|
+
product's actual end state (a change for a human to merge) rather than a benchmark grade.
|
|
945
|
+
**That job goes dark** until the plan shape implements hitl. `soft-green` was never
|
|
946
|
+
implemented past its declaration. **How both come back is deferred to Layer 3 and
|
|
947
|
+
deliberately undecided** — by standing doctrine graduation between rungs is a REWRITE, never
|
|
948
|
+
a copy, so keeping the old implementation alive for a later rung preserved code that would
|
|
949
|
+
never have been copied.
|
|
950
|
+
- **What survived, and where it lives now:** the machinery the plan flow imports was housed in
|
|
951
|
+
the deleted files but was never part of the dead path. The worker tool menu (`TOOL_BY_VERB`,
|
|
952
|
+
the ctx tools, the gate action translator, the personas and strategy lines) moved
|
|
953
|
+
`src/interpret.js` → **`src/tools.js`**, which is what it always was; the fence and secret
|
|
954
|
+
primitives (`globToPrefix`, `scopeContained`, `sweepSecretLiterals`, `SECRET_PATTERNS`,
|
|
955
|
+
`scanSecrets`) stay in `src/validate.js`, now purely the shared primitives both validators
|
|
956
|
+
go through. The ledger's prose verb-sniff list was relocated VERBATIM rather than re-derived,
|
|
957
|
+
so incident classification is byte-identical across the deletion.
|
|
958
|
+
- **Coverage was re-pointed, not dropped.** The suite goes 616 → 395 tests because the tests
|
|
959
|
+
of a deleted path die with it; every guard that OUTLIVED the path was moved rather than
|
|
960
|
+
deleted — the approval binding, the pre-token smoke and four money-contract readings are
|
|
961
|
+
ports onto the plan shape in `tests/run.test.js`, the fence/secret assertions were re-pointed
|
|
962
|
+
at the primitives themselves in `tests/validate.test.js`, and the close-body rules
|
|
963
|
+
(`judged`, `gapKeep`, quotes-in-cmd, secret sweep) now address the plan close. Guards that
|
|
964
|
+
simply moved house are named where they now live: `close-unsupported`, `broken-close`,
|
|
965
|
+
`already-green` and the close's cwd in `tests/planrun.test.js`; `close-crashed` and the
|
|
966
|
+
close-output scrub in `tests/ralph.test.js`.
|
|
967
|
+
|
|
968
|
+
### Added
|
|
969
|
+
- **T — a run has a CLOCK, and time is a material the planner is told as a balance**
|
|
970
|
+
(PRD v1.27/v1.29, design record 2026-07-26 + addenda 1–3). Money was the only bound in the
|
|
971
|
+
system; nothing bounded TIME, and job #5's own plan authorised ~630 worker turns (F56).
|
|
972
|
+
- `maxWallMs` joins the SIGNED job spec (`src/job.js`) — integer ms, **no default**, floor
|
|
973
|
+
one close timeout (`MIN_WALL_MS`). A run without it is time-unbounded *by explicit
|
|
974
|
+
operator choice*, never by an unnoticed fallback (F45: a defaulted cap is a silent second
|
|
975
|
+
ceiling). Adding it changes the spec hash — re-sign.
|
|
976
|
+
- New `src/clock.js` (`createClock`, `PROVIDER_TIMEOUT_MS`, `MIN_CALL_TIMEOUT_MS`): one
|
|
977
|
+
wall clock per run. Enforcement is a **between-round deadline** because `loop.stop()`
|
|
978
|
+
cannot cut an in-flight call — F61 measured it fired at 500ms and returned at 4,018ms —
|
|
979
|
+
so the honest enforced worst case is `maxWallMs + closeTimeoutMs` and BOTH numbers are
|
|
980
|
+
reported (`wall-clock`, `report()`), never the flattering one. Unbounded reports as
|
|
981
|
+
`null`, never `0` and never `Infinity` (F6 extended to time: an unknown duration is
|
|
982
|
+
reported as unknown).
|
|
983
|
+
- `runPlan` emits `wall-clock` (at start), `wall-bounded` (an attempt cut by the deadline —
|
|
984
|
+
the same seam the round bound rides; the partial work is judged and its gap fed forward)
|
|
985
|
+
and terminates `wall-halt` with a decision-ready escalation naming the requested vs
|
|
986
|
+
elapsed ms. The stop IS the checkpoint: raise `maxWallMs` and rerun.
|
|
987
|
+
- **Materials at draft** — `runPlan` emits `materials` and the plan/replan prompt now
|
|
988
|
+
carries `{ balanceUsd, remainingMs }`: what is LEFT, as a balance, never a per-round rate
|
|
989
|
+
(hamr's correction, PRD v1.29). F57 measured a 150× spread on verification-gap duration,
|
|
990
|
+
so a per-round constant describes almost no real round and a rate framing creates a
|
|
991
|
+
rush-or-fake incentive. The agent sees both axes; the arbiter is untouched — materials go
|
|
992
|
+
to the PLANNER at draft, and the worker stays blind (no budget, no close, no clock).
|
|
993
|
+
- **A — the replan trigger gains a VARIANCE axis** (design record §3.4): a step that has
|
|
994
|
+
consumed `VARIANCE_THRESHOLD` (0.5, hamr's number) of the run's *remaining* money or time
|
|
995
|
+
with its exits still red is stopped at the head of its next attempt and routed as a
|
|
996
|
+
`step-variance` replan — the planner re-allocates what is left instead of the step eating
|
|
997
|
+
the run. Emits `variance {step, iteration, threshold, moneyShare, timeShare, axis}`. The
|
|
998
|
+
hard ceiling of **ONE** replan is unchanged (unlimited replanning launders thrash as
|
|
999
|
+
adaptation, v1.12); a second `step-variance` after the replan is spent is a STOP.
|
|
1000
|
+
- **Recorded honestly: this trigger is INERT on every workload with data (F63).** Replayed
|
|
1001
|
+
across 18 archived spines / 54 steps / 101 judged attempts it would have fired **0
|
|
1002
|
+
times** (near misses 0.35 · 0.35 · 0.40 · 0.45), and the premise it was built on —
|
|
1003
|
+
F56's *"a replan has never fired in the entire programme"* — is FALSE: eight replans had
|
|
1004
|
+
already fired, four days before F56 was written.
|
|
1005
|
+
- **Threshold decided at 0.5 with that inertness on the table** (hamr, *"keep 0.5"*, PRD
|
|
1006
|
+
v1.31). It is therefore a guard set ABOVE the observed population, not a tuned trigger —
|
|
1007
|
+
moving it to 0.35 to catch those four points would be fitting the number to the data.
|
|
1008
|
+
- **The staged close — the user authors the DESTINATION, never the road** (PRD v1.28, hamr:
|
|
1009
|
+
*"there shouldn't be user authoring anywhere, that defies the point of bareloop"*). `checks[]`
|
|
1010
|
+
is retired outright: a spec that still declares it reds `checks-derived` **by name**, not a
|
|
1011
|
+
bare `unknown-field`. In its place the operator's close becomes an ORDERED LIST of NAMED
|
|
1012
|
+
STAGES, and the check menu the agent picks from is DERIVED from that list — nobody
|
|
1013
|
+
hand-authors a check per job, ever.
|
|
1014
|
+
- `close: [{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (`src/job.js`) —
|
|
1015
|
+
kebab-case unique names, every stage a `predicate` command body (a rubric/hitl stage is
|
|
1016
|
+
inexpressible by shape); `needs` must name EARLIER stages and is incoherent with
|
|
1017
|
+
`offer: false` on the same stage (a chain with nothing to reach it). A single `predicate`
|
|
1018
|
+
close object is legal shorthand the plan flow adapts into a one-stage list; a
|
|
1019
|
+
`gold`/`rubric`/`hitl` object close still validates (the declared-but-locked verdict
|
|
1020
|
+
classes) but the plan flow refuses it at runtime as `close-unsupported` — it names no
|
|
1021
|
+
command to run. `checkMenu(close)` (`src/job.js`) derives the offerable stages, each
|
|
1022
|
+
carrying the ordered chain (prerequisites, then itself) that a `check-passes(name)` pick
|
|
1023
|
+
must run.
|
|
1024
|
+
- `runStages` (`src/ralph.js`) runs a stage list as ONE close: first red renders the verdict,
|
|
1025
|
+
later stages never run; the gap names the failing STAGE, never a culprit file (v1.12/F28
|
|
1026
|
+
untouched); the secrets scrub runs on every stage.
|
|
1027
|
+
- `runPlan` (`src/planrun.js`) wires the close precheck, the check-menu preflight (now
|
|
1028
|
+
emitting `check-menu {offered, hidden?, meaning?}` — a partial menu is a stage that cannot
|
|
1029
|
+
stand alone as a ruler, never a failure), the `check-passes` seam, the outer close, and the
|
|
1030
|
+
fix loop through `runStages`; `validatePlan` (`src/plan.js`) derives its legal
|
|
1031
|
+
`check-passes` names from `checkMenu(job.close)` instead of a separately-signed list. Every
|
|
1032
|
+
job spec in `jobs/` migrated to the staged shape.
|
|
1033
|
+
- **Two decisions taken 2026-07-26 (hamr, *"agreed on both, validate your claims"*), built and
|
|
1034
|
+
validated:**
|
|
1035
|
+
- **Layer R's red-set is the FAILING STAGE's, per attempt.** A staged close has N
|
|
1036
|
+
`gapKeep`s, so `createRoot().observe()` (`src/root.js`) now takes `redStage`/`redKeep`
|
|
1037
|
+
and REFUSES to compare red-sets across a stage change (a different wall is a different
|
|
1038
|
+
failure — the same bucket as one-known-one-UNKNOWN, never a fire). `root-injected` gains
|
|
1039
|
+
an optional `redStage`. **Measured, not assumed:** the rejected alternative — one union
|
|
1040
|
+
`gapKeep` pattern fixed for the run — was rejected because job #4's two close stages are
|
|
1041
|
+
near-clones of one grader (7 of 12 kept-line templates byte-identical), so a union
|
|
1042
|
+
kept-set can read identical across a stage change and false-fire, which "inert when not
|
|
1043
|
+
stuck" forbids.
|
|
1044
|
+
- **The grading stage is not lendable.** Of the six specs in `jobs/`, the four with a final
|
|
1045
|
+
`verdict` stage set `offer: false` on it; the other two have no grading stage at all and
|
|
1046
|
+
hide a `changed-from-seed` PRECONDITION instead. The flag is kept **per-spec**, not as a
|
|
1047
|
+
schema rule — "the last stage is the grade" holds where it applies and is unproven in
|
|
1048
|
+
general. `check-menu` preflight always
|
|
1049
|
+
announces the derived menu either way, so a forgotten flag is visible in the run's own
|
|
1050
|
+
record; a test reads every spec in `jobs/` and reds if any offers a stage NAMED `verdict`.
|
|
1051
|
+
The guard is stated over the convention that exists rather than over position — the fifth
|
|
1052
|
+
spec proved "the last stage is the grade" is not structural.
|
|
1053
|
+
- **Scripted-provider evidence only — the staged close has not yet run against a real
|
|
1054
|
+
model.**
|
|
1055
|
+
|
|
1056
|
+
### Changed
|
|
1057
|
+
- **The `tree-changed` scope is now a MENU the agent picks from, not a glob it authors and
|
|
1058
|
+
guesses the shape of** (design record 2026-07-26 §4, hamr's *"did agent choose from a list or
|
|
1059
|
+
not?"*). F60 measured **13 of 18 validator reds as this one class**: the agent writes
|
|
1060
|
+
`{"type":"tree-changed","scope":"src/*.js"}`, the grammar accepts only a trailing `/**` or
|
|
1061
|
+
`/*`, and every red costs a redraft call. Teaching the rule in the prompt or widening the
|
|
1062
|
+
grammar both leave the agent authoring free text; enumerating makes an illegal scope
|
|
1063
|
+
**inexpressible**.
|
|
1064
|
+
- New `legalScopes(writeScope, dirs, cap?)` (`src/plan.js`) is the ONLY producer of scope
|
|
1065
|
+
menus: the signed fence entries (always offerable — they cover directories a step has not
|
|
1066
|
+
created yet) plus the real directories beneath them, fence-filtered, deduped,
|
|
1067
|
+
shallowest-first, capped at `MAX_SCOPE_MENU` (24). Because it filters by the fence, a menu
|
|
1068
|
+
entry that escapes containment cannot be constructed rather than being caught downstream.
|
|
1069
|
+
- `planPrompt` gains a `scopes` parameter and lists the values verbatim, one per line;
|
|
1070
|
+
`validatePlan` gains `opts.scopes` and accepts **membership only**. Omitting the option
|
|
1071
|
+
derives the menu from the signed `writeScope` — fail-CLOSED, never a free-text containment
|
|
1072
|
+
fallback (F50: a silently-ignored optional param is the blind-instrument class).
|
|
1073
|
+
- An off-menu scope **splits by cause**: outside the signed fence stays `scope-escape` (a
|
|
1074
|
+
behaviour signal the ledger keys on), in-fence-but-unoffered is `invalid-value` carrying the
|
|
1075
|
+
menu so the redraft can choose. Collapsing them would launder an attempted fence escape into
|
|
1076
|
+
a typo class.
|
|
1077
|
+
- `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` — no silent caps.
|
|
1078
|
+
- `globToPrefix` is **untouched**: this changes what the agent is offered, never what the
|
|
1079
|
+
fence enforces.
|
|
1080
|
+
|
|
1081
|
+
### Fixed
|
|
1082
|
+
- **F64 — a wall-derived call timeout was reported as a network failure**, so the operator's own
|
|
1083
|
+
governance stop was filed as a transport casualty (never evidence, retried, carrying the F44
|
|
1084
|
+
`spendComplete:false` floor) and the human was told to fix the provider binding. `clock.callTimeoutMs()`
|
|
1085
|
+
bounds each provider call by the time left, and bare-agent's `TimeoutError` carries a code but no
|
|
1086
|
+
category — which the runner defaulted to `provider-red` at four separate seams.
|
|
1087
|
+
- New `isWallTimeout(err, clock)` + `TIMEOUT_CODE` (`src/clock.js`) is the whole discriminator: an
|
|
1088
|
+
`ETIMEDOUT` on an **expired** clock is the wall's own stop. One `categorize()` inside `runPlan`
|
|
1089
|
+
is now the ONLY place an uncategorised throw is classified, so the worker seam, the native seam,
|
|
1090
|
+
the scout/drafting relay and the close-fix loop cannot drift apart. A throw that named its own
|
|
1091
|
+
category still keeps it.
|
|
1092
|
+
- Every wall stop emits ONE record shape, with `cutMidCall` splitting a deadline that landed
|
|
1093
|
+
inside a provider call from one read between steps; `ralph` gained a `wall-halt` decision entry
|
|
1094
|
+
so a governance stop no longer reads as "the middle broke", and its options are raise-the-cap,
|
|
1095
|
+
not retry-the-provider. `runPlan` can now return `wall-halt` from the drafting and fix paths too.
|
|
1096
|
+
- **The controls are part of the fix:** the same error with time still on the clock stays
|
|
1097
|
+
`provider-red` (a real dead socket is never laundered into a governance stop), and an unbounded
|
|
1098
|
+
run can never produce a `wall-halt`. Stated limit: a genuine hang on a run whose wall has ALSO
|
|
1099
|
+
passed reads as `wall-halt` — the run is out of time either way and the remedy is identical.
|
|
1100
|
+
- Also closed a coverage hole found en route: the **between-steps** wall terminal had no test at
|
|
1101
|
+
all since it shipped. `runPlan` gains an injected `now` (the seam `createClock` already exposes)
|
|
1102
|
+
because `maxWallMs`'s floor is one close timeout, so real time cannot exercise the terminal.
|
|
1103
|
+
- **F59 — the SCOUT returned nothing on 15 of 18 runs, so the planner drafted blind.** The
|
|
1104
|
+
round bound is enforced from the metering callback (`loop.stop`), so a scout still calling
|
|
1105
|
+
tools on its last round was halted mid-tool-use and its `text` was empty: it spent its whole
|
|
1106
|
+
allowance exploring and never wrote the survey that is its only deliverable. `planPrompt`
|
|
1107
|
+
then substituted `(no scout notes)` — a legal prompt, so the failure was silent. Measured
|
|
1108
|
+
across every archived plan-v1 run: 15 of 16 bounded scouts produced 0–86 bytes; both scouts
|
|
1109
|
+
that finished on their own produced full 6–8KB surveys. Two fixes:
|
|
1110
|
+
- **The summary round is now reserved.** A bounded-and-short scout gets one **toolless**
|
|
1111
|
+
round on the same conversation (`askFrom`), where text is the only possible output — a
|
|
1112
|
+
mechanical guarantee, never a prose request (F19/F37: a persona pacing mandate was violated
|
|
1113
|
+
6/6). Native is excluded by design: a toolless native session reports no cost (F48), so a
|
|
1114
|
+
recovery round there would be unmetered spend.
|
|
1115
|
+
- **An empty survey is now loud.** `scout-empty` is a named spine event. It is never a halt —
|
|
1116
|
+
3 of 5 archived greens had an empty scout, so failing the run there would be a worse error
|
|
1117
|
+
than the one being fixed.
|
|
1118
|
+
- `scoutRounds` is now a shell-owned option (like `capRuns`/`maxStepRounds`).
|
|
1119
|
+
- **Confirmed live:** the patient that returned 0 bytes returned 5379 bytes after the fix.
|
|
1120
|
+
F60 then measured what that survey is worth: it carries ~15× more real repository
|
|
1121
|
+
specifics into the plan, and roughly triples the plan's declared work.
|
|
1122
|
+
|
|
1123
|
+
### Changed
|
|
1124
|
+
- **bare-agent 0.33.1 → 0.34.0** (BA-18 delivered). The provider now bounds socket
|
|
1125
|
+
**inactivity** itself (`timeoutMs`, 10-minute default, per-call overridable, rejecting with a
|
|
1126
|
+
retryable `TimeoutError`/`ETIMEDOUT`), so a silently-dropped socket is a 10-minute error
|
|
1127
|
+
instead of a ~2-hour hang. The harness-side stale-socket guards in `scripts/run-screen-types.mjs`
|
|
1128
|
+
and `scripts/probe-materials.mjs` are deleted as superseded. **`Retry` stays deliberately
|
|
1129
|
+
unwired:** a BA-14 `EPIPE` dies on write so the request never reached the API and a retry is
|
|
1130
|
+
free, but a timed-out request may have been accepted and processed — retrying pays twice for
|
|
1131
|
+
one completion. A trip is a clean `provider-red`, and the escalation's own "retry the run"
|
|
1132
|
+
option recovers it without the double-bill.
|
|
1133
|
+
- `planPrompt` is exported from `src/planrun.js` (in-repo seam for probe drivers; **not** added
|
|
1134
|
+
to `src/index.js`, so the adopter surface is unchanged). Probes now import the shipped prompt
|
|
1135
|
+
instead of copying it — the N3 pre-probe's verbatim copy was diffed against source and found
|
|
1136
|
+
identical, so F51–F55 stand, but a copy is a fixture and new drivers do not take that risk.
|
|
9
1137
|
|
|
10
1138
|
## [0.5.1] — 2026-07-24
|
|
11
1139
|
|