bareloop 0.3.0 → 0.5.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 +194 -0
- package/README.md +12 -5
- package/bareloop.context.md +167 -16
- package/package.json +2 -2
- package/src/exits.js +163 -0
- package/src/index.js +7 -1
- package/src/interpret.js +192 -25
- package/src/job.js +231 -88
- package/src/plan.js +237 -0
- package/src/planrun.js +677 -0
- package/src/ralph.js +25 -7
- package/src/root.js +305 -0
- package/src/run.js +88 -22
- package/types/exits.d.ts +51 -0
- package/types/index.d.ts +4 -1
- package/types/interpret.d.ts +107 -1
- package/types/job.d.ts +12 -0
- package/types/plan.d.ts +41 -0
- package/types/planrun.d.ts +49 -0
- package/types/ralph.d.ts +21 -3
- package/types/root.d.ts +96 -0
- package/types/run.d.ts +26 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,200 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.0] — 2026-07-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Layer 2 core — the plan-v1 flow (design record
|
|
15
|
+
`docs/plans/2026-07-21-layer-2-plan-v1-design.md`; premise validated in F46).**
|
|
16
|
+
The semantic converter, built:
|
|
17
|
+
- **job-v1 four-field plan shape** (`src/job.js`): `goal` / `verdictType`
|
|
18
|
+
(`green|soft-green|hitl` frozen radio — v1 admits `green` only; a locked type is a
|
|
19
|
+
`request-red` with the type as a structured `verb` field) / `close` / `checks[]`
|
|
20
|
+
(operator-SIGNED named checks: the predicate-close body + a slug name, same
|
|
21
|
+
validation, same runClose machinery; checks decide nothing and mint nothing) /
|
|
22
|
+
`tools` (the plan ceiling). Exclusive with legacy `steps[]` (`shape-conflict` red);
|
|
23
|
+
`steps[]` is co-existing scaffolding with a staged sunset — archives alongside
|
|
24
|
+
config-v1 when the Layer 2 path proves itself in its battery.
|
|
25
|
+
- **plan-v1 validator** (`src/plan.js`, `validatePlan`): gates the AGENT-authored
|
|
26
|
+
plan against the SIGNED spec — verbs ⊆ ceiling (`verb-escape`, verb as structured
|
|
27
|
+
data), rounds ≤ shell cap, targets/scopes inside the fence, exits from the closed
|
|
28
|
+
menu only (`exit-illegal`), `check-passes` resolving against the signed menu
|
|
29
|
+
(`check-unknown` names the menu), no `dependsOn` (strictly sequential — an inert
|
|
30
|
+
knob is a fake contrast lever), AND-only exit composition max 2, and the F17
|
|
31
|
+
pairing law (check-passes on a write step demands tree-changed — the seed tree is
|
|
32
|
+
green). Fails CLOSED on a missing/non-plan-shape job (`job-invalid`).
|
|
33
|
+
- **exit evaluator** (`src/exits.js`, `snapshotScope`/`evalExits`): the shell's own
|
|
34
|
+
fixed code for `artifact-written` / `tree-changed` / `json-valid` /
|
|
35
|
+
`check-passes`. Outcome, never intent: sha256 snapshots, identical re-writes are
|
|
36
|
+
not changes (F43), git status never consulted (F45). Instrument faults ride out as
|
|
37
|
+
`fault` by runClose verdict name — escalated, never fed to the worker as a gap.
|
|
38
|
+
- **the judge seam** (`src/ralph.js`): `ralph({ judge })` — the PRD v1.12 §4
|
|
39
|
+
generalization; shell-injected, inexpressible in any config or plan. Same verdict
|
|
40
|
+
vocabulary, so the forbidden zone, F32 worker-crash routing (a check crashed by the
|
|
41
|
+
worker's own test feeds back — the F46 mechanism), and cap taxonomy are unchanged.
|
|
42
|
+
- **the plan executor** (`src/planrun.js`, `runPlan`): close precheck
|
|
43
|
+
(`already-green` distinct, F17) → checks preflight ($0, before tokens) → read-only
|
|
44
|
+
SCOUT → PLAN (one redraft with reds fed back) → sequential micro-loops with
|
|
45
|
+
exit-gap feedback → ONE replan (exhaustion only) → the operator's close with one
|
|
46
|
+
bounded fix loop. Every round metered `worker-round` with a phase label (F12);
|
|
47
|
+
prompt contract v1.12 §5 mutation-proven; `plan-executed` (plan-as-executed,
|
|
48
|
+
design law #2) on the spine.
|
|
49
|
+
- **runJob dispatch** (`src/run.js`): a plan-shape spec routes through the ONE
|
|
50
|
+
runJob entry — same approval gate, smoke, ledger, and job-end money contract.
|
|
51
|
+
New outcomes: `already-green | plan-red | check-red | close-red`.
|
|
52
|
+
- 139 new tests (503 total), TDD throughout, 3 targeted mutations fired and killed.
|
|
53
|
+
Built and integration-tested against scripted providers.
|
|
54
|
+
- **Layer 2 rung ACCEPTED — the real-model acceptance battery (F47; prereg
|
|
55
|
+
`docs/02-experiments/TESTGEN-PREREG.md` §2026-07-22a/b).** Job #4 (TESTGEN) run
|
|
56
|
+
through the REAL plan flow (`runJob → runPlan`: scout → the agent DRAFTS the plan →
|
|
57
|
+
validator gates → per-step check-loops → outer grader), on `anthropic-api`,
|
|
58
|
+
claude-sonnet-5, vs F39's baseline (0 conversion) and the F46 POC (hardwired).
|
|
59
|
+
**3/3 valid acting rows converted (≥2/3 bar) → accepted; 3/3 cleared the 45% bar
|
|
60
|
+
(67.5/55/55, vs the POC's 27.5/40/37.5 with 0 at 45); 3/3 the agent composed the
|
|
61
|
+
`check-passes(clean-run)` exit ITSELF** (the one thing the POC could not test). Every
|
|
62
|
+
green driven by the step check-loop alone; all writes fenced, source frozen, secrets
|
|
63
|
+
clean; the F45 spend guard stopped an unpriced casualty. 7 provider-red casualties
|
|
64
|
+
across an Overloaded window (excluded as evidence), $27.36 of a $30 cap. This trips
|
|
65
|
+
the "path closes green end-to-end" milestone: **`steps[]` and config-v1 sunset on
|
|
66
|
+
landing.** Driver `scripts/run-battery-l2accept.mjs` (gained `--need`/`--priorUsd`
|
|
67
|
+
for a multi-run continuation under one governed cap).
|
|
68
|
+
- **Module 4d — native clipipe worker surface (BA-16; `bare-agent` → `^0.33.0`).** The
|
|
69
|
+
plan flow now runs on TWO surfaces: the `Loop` (API, unchanged) and, when
|
|
70
|
+
`job.provider === 'clipipe-subscription'`, the `claude` CLI's NATIVE tool channel — the
|
|
71
|
+
subscription path (no metered API). Since native governance is constructor-time and
|
|
72
|
+
per-worker, the runner takes a `nativeProvider` FACTORY (`{policy, onTurn?, maxTurns,
|
|
73
|
+
hasTools}) => provider`) it calls fresh per worker: `hasTools:true` → native tool mode
|
|
74
|
+
(the SAME `wireGate` fence clips onto the provider — a live POC proved an out-of-scope
|
|
75
|
+
write is DENIED); `hasTools:false` → the toolless drafter runs metered claude-json TEXT
|
|
76
|
+
mode (a native session reports no cost — this path keeps the drafter's spend visible).
|
|
77
|
+
Money reconciles per session (accounted `worker-round` = session total; `worker-turn` =
|
|
78
|
+
attribution); `max_turns` is a bounded attempt, not an escalation; a missing factory is
|
|
79
|
+
`interpreter-red`, never a silent fall-back to the API. 5 native tests + a live
|
|
80
|
+
end-to-end smoke on the real CLI (green, all workers metered, `spendComplete` honest).
|
|
81
|
+
|
|
82
|
+
- **Native read-cap — the CLI truncation fix (F48; `bare-agent` → `^0.33.1`, BA-17
|
|
83
|
+
ranged read).** On the `clipipe-subscription` surface the `claude` CLI truncates a large
|
|
84
|
+
tool result (~40–50KB, measured) BEFORE the model sees it — spilling the remainder to a
|
|
85
|
+
fenced-off `tool-results/` file and wrapping it in a "read in chunks" notice the model
|
|
86
|
+
distrusts as injection — so a whole-file `shell_read` of a large file blinded the native
|
|
87
|
+
worker (0-write stall). The runner now bounds the native `shell_read` result below the CLI
|
|
88
|
+
cap (`NATIVE_READ_CAP`) and returns a TRUSTED notice steering to `ctx_get` ranged retrieval,
|
|
89
|
+
plus a native-only strategy line; the API path is untouched (full result rides into context).
|
|
90
|
+
Measured: **0 → 7 writes** on the real job. Cross-surface verdict (F48): the native surface
|
|
91
|
+
is capable at the STEP but did not carry job #4 to a grade — 0/2 acting rows vs the API's
|
|
92
|
+
3/3, and a 3.5× budget raise ($8→$28) was refuted (escalated on the F39 semantic-stall at
|
|
93
|
+
$7). IN only as a babysat, $0-marginal-billing fallback; **only the `anthropic-api` surface
|
|
94
|
+
is guaranteed.** Local LLMs remain deferred and unmeasured.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **Layer 2 pre-release review (F48) — 4 fixes, 2 correctness** (TDD, failing-then-passing
|
|
99
|
+
test each; full suite 519/519): (1) a provider-red/gate-red raised DURING a step's micro-loop
|
|
100
|
+
was collapsed to `step-red:<id>` — a transport CASUALTY recorded as a capability failure and
|
|
101
|
+
missing the F44 `spendComplete:false` floor; each terminal category now rides out under its own
|
|
102
|
+
name so the returned outcome and the emitted escalation agree (F11). (2) `tree-changed` counted
|
|
103
|
+
a sibling scope's files as deletions when a step had ≥2 tree-changed exits (merged snapshot),
|
|
104
|
+
falsely passing an unchanged scope — deletions are now scoped to the exit's own prefix. (3)
|
|
105
|
+
abandoned-plan artifacts no longer ride forward as the new plan's "prior steps' results" after a
|
|
106
|
+
replan. (4) dead `isUnpriced()` sub-conditions removed from the replan/cap-halt terminal (the
|
|
107
|
+
step-end guard already returns `pricing-red` first).
|
|
108
|
+
- **Layer 2 whole-branch review — 8 doctrine-restoring fixes to the plan flow** (all in the
|
|
109
|
+
graduated Layer 2 code, none in pre-existing modules; validated against source with 0
|
|
110
|
+
refuted, a failing-then-passing test each): a `gold` close validated under `verdictType:
|
|
111
|
+
green` crashed `runPlan` with no `job-end` (now `close-unsupported` before tokens); the
|
|
112
|
+
`check-passes` gap was re-truncated to 400 chars, deleting the gapKeep failing-test names
|
|
113
|
+
(F28 reintroduced — now carried whole); no in-flight `pricing-red` (F6 — now bails at
|
|
114
|
+
scout/plan/step); the plan drafter's Gate budget was frozen pre-execute and reused for the
|
|
115
|
+
replan (now a fresh drafter per `obtainPlan`); a money-gate halt triggered a replan instead
|
|
116
|
+
of stopping (F45 — now gated on funds, drained → honest `cap-halt`); the step-setup catch
|
|
117
|
+
recorded a category the escalation contradicted (F11 — now agreed); the plan branch dropped
|
|
118
|
+
F44's `spendComplete:false` on a transport-throw `provider-red`; a write-only tool ceiling
|
|
119
|
+
validated, blinding the scout (now requires ≥1 read-capable verb).
|
|
120
|
+
|
|
121
|
+
## [0.4.0] — 2026-07-21
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- **Layer R — the root, the within-run ratchet (`src/root.js`; design record
|
|
126
|
+
`docs/plans/2026-07-19-layer-r-design.md`, interview-locked 2026-07-19).** The shell
|
|
127
|
+
mechanically detects fixation — consecutive attempts rewriting the same file(s)
|
|
128
|
+
without moving the close's kept-failure set — and injects an escalating note into the
|
|
129
|
+
next attempt's prompt: a capped summary first, then the worker's own prior failed
|
|
130
|
+
edit content verbatim (the BA-14 rejected-edit-buffer shape, rewritten for our
|
|
131
|
+
attempt loop). Fixation-gated (inert when not stuck — RSI §3.3: the lift is a
|
|
132
|
+
fixation phenomenon, an honest null on a strong unstuck model), shell-authored (the
|
|
133
|
+
worker authors nothing, gains no verb), within-run only (state dies with the run;
|
|
134
|
+
inheritance stays verdict-gated). Red-set comparison strips the spec-reporter's
|
|
135
|
+
per-run duration stamps (POC-measured: kept lines are never byte-stable) —
|
|
136
|
+
comparison-only, the delivered gap is untouched. Spine event `root-injected` carries
|
|
137
|
+
stage/mode/streak/paths, never content (append-only law). Ships **OFF by default**
|
|
138
|
+
(`layerRoot: false` on `runJob`/`interpret`; pass `true` for the ON/experimental arm) —
|
|
139
|
+
decided 2026-07-21: fixation is extinct on every current job (F41), so ON has never won
|
|
140
|
+
its own A/B; the default flip to `true` defers to the first Layer 2 job that produces
|
|
141
|
+
natural fixation (see `docs/01-product/LAYERS.md` Layer R note).
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **F44 — fresh whole-branch review: three correctness bugs + cleanups, all validated and
|
|
146
|
+
mutation-proven.** (1) A casualty job-end laundered unknown spend as complete: `1b0720c`
|
|
147
|
+
put `spentUsd` on every path, so a provider-red TRANSPORT THROW reported
|
|
148
|
+
`spendComplete: true` (F6 violation — the failed call's cost is unknown) and the battery
|
|
149
|
+
scripts' `spentUsd == null` casualty-STOP never fired. Fixed both sides: the transport
|
|
150
|
+
provider-red now reports `spendComplete: false` (the metered draft-truncation keeps
|
|
151
|
+
`true`), and the frozen battery/probe scripts key their STOP on
|
|
152
|
+
`spendComplete === false || spentUsd == null`. (2) Layer R's outcome probe mislabeled a
|
|
153
|
+
missed-anchor edit as "landed" when its `newText` appeared elsewhere (substring
|
|
154
|
+
false-positive) — now discriminated by the tool result, exact-equality for writes, one
|
|
155
|
+
read not two. (3) Layer R false-fired on a progressing text-mode worker with no gapKeep
|
|
156
|
+
(write-overlap is constant-true when the single target is rewritten every attempt) — a
|
|
157
|
+
`writesInformative` flag now requires a known-unmoved red-set when writes carry no
|
|
158
|
+
information. Cleanups: double redaction, unbounded `attempts` retention, a dead ternary.
|
|
159
|
+
(2) and (3) are latent behind the OFF-by-default `layerRoot`.
|
|
160
|
+
|
|
161
|
+
- **Layer R settled its note on the gate's allow, which is written BEFORE the tool runs
|
|
162
|
+
(F43).** An `allow` record states INTENT, never that bytes reached a file:
|
|
163
|
+
`shell_edit` returns an anchor miss as a refusal *result* and a byte-cap overflow as a
|
|
164
|
+
throw, both leaving the file untouched with the allow already on the audit. The
|
|
165
|
+
verbatim note could therefore present content to the worker as "your own previous
|
|
166
|
+
changes — they landed" while that text was provably absent from the file it named
|
|
167
|
+
(reproduced end-to-end: 3 allowed edits, 0 bytes changed). Fixed by splitting the two
|
|
168
|
+
axes rather than merging them — the DETECTOR keeps reading intent (an edit that never
|
|
169
|
+
applied is still repetition; a tree-diff detector measured blind to it on every
|
|
170
|
+
attempt), while the NOTE settles on the observed file through `Loop`'s
|
|
171
|
+
`onToolResult` seam. An unapplied repeat now names the missed anchor instead, which is
|
|
172
|
+
the mechanical gap genre (F38). `commitWrite` is replaced by `settleWrite(landed)`.
|
|
173
|
+
- **Guarded the `maxTurns` LLM-round invariant (F43 follow-up).** bareguard's `maxTurns`
|
|
174
|
+
ticks on every `gate.record`; our cap means "LLM rounds" only because the sole record
|
|
175
|
+
path is the LLM one (tool calls take `gate.check`, which does not tick). That was
|
|
176
|
+
correct but unguarded — wiring tool results into `gate.record` would silently halve the
|
|
177
|
+
LLM budget (the F37 lower-silent-ceiling class). Added a guard test pinning "every
|
|
178
|
+
`gate.record` is `type:llm`" (mutation-proven: wiring tool records turns it red) and a
|
|
179
|
+
config comment marking the invariant load-bearing. No behavior change; no upstream ask —
|
|
180
|
+
bareguard already offers the correct counters.
|
|
181
|
+
- **F41 — the disposition: armed-and-inert, field read deferred.** Before any ON/OFF
|
|
182
|
+
battery spent money, two cheap reads measured the disease's base rate: the archive
|
|
183
|
+
sweep (`poc/layer-r-base-rate.mjs`, $0 — every surviving spine is OFF-arm by
|
|
184
|
+
construction) read 0 fixated in 10 pairs on jobs #2/#4; two frozen probes on a
|
|
185
|
+
rebuilt job #1 patient (`scripts/run-probe-layer-r.mjs`, $10.12 total) read 0
|
|
186
|
+
fixated in 4 pairs — including against a three-plant tree (three subsystems, one fix
|
|
187
|
+
cannot green) that forced a full 8→4→green ladder across three judged attempts.
|
|
188
|
+
Every pair was healthy navigation. F21's fixation was a broken-loop symptom, cured
|
|
189
|
+
by the F20/F21/F30/BA-13 fixes. Because ON has therefore never won its own A/B,
|
|
190
|
+
Layer R ships **OFF by default** (decided 2026-07-21; `layerRoot: true` is the ON
|
|
191
|
+
arm) — measured cost-free when enabled and healthy (zero injections across all
|
|
192
|
+
probe runs); the repetition-drop ON/OFF read, and the default-flip decision, defer
|
|
193
|
+
to the first run whose spine records `root-injected`. No learning claim is minted.
|
|
194
|
+
|
|
195
|
+
### Changed
|
|
196
|
+
|
|
197
|
+
- **`jobs/litectx-maintainer.json` `budgetUsd` 1.5 → 4.5** (probe 2, re-signed): $1.50
|
|
198
|
+
funded exactly ONE judged attempt on the real patient — a run could red at attempt 1
|
|
199
|
+
and die at the gate mid-attempt-2, structurally unable to produce across-attempt
|
|
200
|
+
evidence. The advertised budget must fund the attempts the cap promises.
|
|
201
|
+
|
|
8
202
|
## [0.3.0] — 2026-07-19
|
|
9
203
|
|
|
10
204
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<img src="https://img.shields.io/github/package-json/v/hamr0/bareloop?label=version&color=2a4f8c" alt="version (auto from package.json)">
|
|
15
15
|
<img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
|
|
16
|
-
<img src="https://img.shields.io/badge/status-
|
|
16
|
+
<img src="https://img.shields.io/badge/status-Layer%202%20%C2%B7%20accepted-2a6d3a" alt="status: Layer 2, accepted">
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
**"Automate this job — I don't know the best workflow."** For tasks that are **repeated,
|
|
@@ -25,10 +25,15 @@ inheritance with ledger-counted attribution.
|
|
|
25
25
|
The pitch in one line: **workflows that earn their own design, with receipts** — every
|
|
26
26
|
inherited rule carries the green that minted it and the contrast that attributed it.
|
|
27
27
|
|
|
28
|
-
> **Status:
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
28
|
+
> **Status: Layer 2 — accepted.** The agent now authors its own workflow: `runJob()`
|
|
29
|
+
> takes a signed job spec, surveys the repo, **drafts a validated plan of bounded steps**
|
|
30
|
+
> (each with an operator-signed self-check it may reference but never author), and runs it
|
|
31
|
+
> under an un-gameable outer close and one budget it can only tighten. On the real-model
|
|
32
|
+
> acceptance battery (F47) the emergent flow converted a job the same worker failed 4/4
|
|
33
|
+
> before — 3/3 runs, clearing the owned bar every time, composing its own check exits. The
|
|
34
|
+
> within-run ratchet (Layer R) ships armed-and-inert by measurement (F41). Next: **N3 —
|
|
35
|
+
> executed inheritance**, where the workflow persists and improves across runs (the
|
|
36
|
+
> ladder's kill-switch question).
|
|
32
37
|
|
|
33
38
|
## Quick start
|
|
34
39
|
|
|
@@ -98,6 +103,8 @@ ladder, and the stop is a result.
|
|
|
98
103
|
| **N0** ✅ | Port + outer shell + spine (token-free) — v0.1.0 |
|
|
99
104
|
| **N1** ✅ | Job/close schema + validator — v0.2.0 |
|
|
100
105
|
| **N2** ✅ | Single-job headless loop — job #1 minimal (review→fix→PR, hard greens only) — v0.3.0 |
|
|
106
|
+
| **Layer R** ✅ | Within-run ratchet: shell-side fixation detector (intent) + rejected-edit feedback (outcome), **shipped OFF by default** (F41: fixation in remission, so ON has never won its A/B; F43 split the two axes; field read + default decision defer to Layer 2) — v0.4.0 |
|
|
107
|
+
| **Layer 2** | plan-v1 micro-wheels — the road: bounded steps with in-run verifiable exits (the F39-measured semantic converter) |
|
|
101
108
|
| **N3** | Executed inheritance + contrast-bit extractor — **kill-switch: rules must transmit across non-identical runs** |
|
|
102
109
|
| **N4** | Verdict classes complete (soft/HITL minting) |
|
|
103
110
|
| **N5** | Scheduler + budget ops |
|
package/bareloop.context.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# bareloop — Integration Guide
|
|
2
2
|
|
|
3
|
-
> **Current through
|
|
4
|
-
> draft-PR hitl step
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
3
|
+
> **Current through Layer 2 (ACCEPTED, F47)** (headless single-job loop: `runJob`, text +
|
|
4
|
+
> tool middles, the draft-PR hitl step; the plan-v1 flow — four-field job shape, agent-
|
|
5
|
+
> authored validated plans, in-run check exits — validated end-to-end on the real-model
|
|
6
|
+
> acceptance battery: 3/3 conversion, 3/3 over the owned bar, the agent composing its own
|
|
7
|
+
> check exits). The plan shape is the primary path; legacy `steps[]` co-exists but sunsets
|
|
8
|
+
> on landing. Runs on two worker surfaces — the API `Loop` and, for
|
|
9
|
+
> `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
|
|
10
|
+
> as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture,
|
|
11
|
+
> the refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this
|
|
12
|
+
> file ships with the package and is the complete adopter contract; the README is only
|
|
13
|
+
> the pitch.
|
|
8
14
|
|
|
9
15
|
## What this is
|
|
10
16
|
|
|
@@ -80,6 +86,22 @@ unknown-field reds.
|
|
|
80
86
|
| `steps[].tools` | unique subset of `read\|grep\|write\|edit\|recall\|get` | the SPEC-side tool grant (`TOOL_MENU`, frozen; defaults to the full menu) — the drafted config cannot express mode or tools; `edit` (BA-13) is the anchored exact-once replace, judged by the SAME writeScope fence as `write`; requesting `run` (`LOCKED_TOOLS`) reds with the DISTINCT code `request-red` (locked-but-listed: the red IS the admission evidence the ledger tallies; a typo stays `invalid-value`) |
|
|
81
87
|
| `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
|
|
82
88
|
|
|
89
|
+
**The plan shape (Layer 2, design record 2026-07-21).** A job-v1 spec declares EITHER the
|
|
90
|
+
`steps[]` chain above OR the four-field plan shape — never both (`shape-conflict` red).
|
|
91
|
+
In the plan shape the AGENT authors the step plan at run time (gated by `validatePlan`);
|
|
92
|
+
the human signs only:
|
|
93
|
+
|
|
94
|
+
| field | shape | notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `goal` | non-empty text | what the agent plans against |
|
|
97
|
+
| `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern) |
|
|
98
|
+
| `close` | a close object (table below) | ONE close, the only truth; `green` demands a hard-class close (`close-hierarchy` red on a rubric/hitl close) |
|
|
99
|
+
| `checks` | optional array of `{ name, cmd, expect, judged?, gapKeep? }` | the operator-SIGNED named-check menu: the predicate-close body plus a kebab slug `name`, validated by the same rules, executed under the same runClose machinery. Plans reference them via the `check-passes(name)` exit; the agent can never author, edit, or compose one. **Checks decide nothing and mint nothing** — a check result is a progress gate and a gap source only |
|
|
100
|
+
| `tools` | optional unique subset of `TOOL_MENU` | the CEILING every plan step's grant must fit inside (defaults to the full menu); `run` reds `request-red` here exactly as on a step grant. On a legacy `steps[]` spec this field is an `invalid-value` red (steps grant tools per step) |
|
|
101
|
+
|
|
102
|
+
`steps[]` is co-existing scaffolding with a staged sunset: it archives alongside
|
|
103
|
+
config-v1 once the Layer 2 path proves itself in its battery — not before.
|
|
104
|
+
|
|
83
105
|
**Close types and the hierarchy** (a close is data, never code; verdict-class laundering
|
|
84
106
|
is a named red `close-hierarchy`):
|
|
85
107
|
|
|
@@ -140,18 +162,36 @@ drafted workflow config cannot express it (unknown-field red). The gap path also
|
|
|
140
162
|
**both** streams (stdout + stderr), so a failure printed to stdout survives stderr noise
|
|
141
163
|
(F28's adjacent hazard — the old `err || out` returned stderr alone and lost it).
|
|
142
164
|
|
|
143
|
-
|
|
165
|
+
**The plan document (`schema: "plan-v1"`, AGENT-authored — the only document the emergent
|
|
166
|
+
middle writes; `validatePlan` gates it against the SIGNED job spec before tokens burn):**
|
|
167
|
+
|
|
168
|
+
| field | shape | notes |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `steps` | ordered array, 1..8 (`MAX_PLAN_STEPS`) | strictly sequential — array order IS the order; no `dependsOn` (unknown-field red: an inert knob is a fake contrast lever) |
|
|
171
|
+
| `steps[].id` | kebab slug, unique | |
|
|
172
|
+
| `steps[].action` | non-empty text | the step's task — the worker sees only this step |
|
|
173
|
+
| `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
|
|
174
|
+
| `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
|
|
175
|
+
| `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
|
|
176
|
+
| `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. `check-passes` must name a SIGNED check (`check-unknown` red names the signed menu); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
|
|
177
|
+
|
|
178
|
+
Red vocabulary (all three validators): `parse-error`, `unknown-field`, `missing-required`,
|
|
144
179
|
`invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
|
|
145
|
-
`secret-literal`, `scope-escape`, `fence-invalid` (a malformed `jobWriteScope` fence — attributed to `jobWriteScope`, never the workflow config),
|
|
146
|
-
`
|
|
180
|
+
`secret-literal`, `scope-escape`, `fence-invalid` (a malformed `jobWriteScope` fence — attributed to `jobWriteScope`, never the workflow config), `shape-conflict` (both job shapes declared),
|
|
181
|
+
`request-red` (locked-but-listed: a locked tool or verdictType — admission demand the
|
|
182
|
+
ledger tallies), plus the workflow-side verb reds (`verb-illegal`,
|
|
183
|
+
`verb-placement`, `verb-params`, `slot-overflow`) and the plan-side reds (`verb-escape`,
|
|
184
|
+
`exit-illegal`, `check-unknown`, `job-invalid` — a plan validated against a missing or
|
|
185
|
+
non-plan-shape job fails CLOSED). The `secret-literal` sweep is
|
|
147
186
|
defense-in-depth against known token shapes — env-only loading remains the law, not the
|
|
148
187
|
sweep.
|
|
149
188
|
|
|
150
189
|
## Public API
|
|
151
190
|
|
|
152
|
-
*Landed through N2 (spine + shell +
|
|
153
|
-
+ extractor + runJob). Still TBD: N3
|
|
154
|
-
|
|
191
|
+
*Landed through N2 + the Layer 2 core (spine + shell + three validators + interpreter
|
|
192
|
+
with text/tool middles + the plan executor + extractor + runJob). Still TBD: N3
|
|
193
|
+
(contrast-bit extractor live), N4 (verdict classes — gold/rubric close EXECUTION), N5
|
|
194
|
+
(scheduler + budget ops + CLI), N6 (panel).*
|
|
155
195
|
|
|
156
196
|
### `makeSpine(file)` → `emit(type, data?)` — `src/spine.js`
|
|
157
197
|
|
|
@@ -159,7 +199,7 @@ Append-only JSONL event emitter bound to one file. `seq` monotonic per spine, `t
|
|
|
159
199
|
last. Consumers are pure listeners; nothing reads the file back. Returns each event as
|
|
160
200
|
written.
|
|
161
201
|
|
|
162
|
-
### `ralph({ middle, close
|
|
202
|
+
### `ralph({ middle, close?, judge?, capRuns, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
163
203
|
|
|
164
204
|
The dumb outer shell: `while close-red and under-cap: run the middle`. `close` is an argv
|
|
165
205
|
whose exit code is truth (`runClose` is also exported); the red gap text feeds the next
|
|
@@ -205,6 +245,15 @@ to the gate audit's allow-decision write/edit lines (run_id-scoped); no seam or
|
|
|
205
245
|
keeps the old behavior — an instrument crash still escalates `close-crashed`, never retried.
|
|
206
246
|
Measured motivation: battery pass 1 (F31) lost 4 of 7 rows to exactly this escalation.
|
|
207
247
|
|
|
248
|
+
**The judge seam (Layer 2, PRD v1.12 §4).** `judge?: async () => {verdict, gap?, detail?}`
|
|
249
|
+
— a SHELL-injected replacement for `runClose`, so a plan step's micro-loop is judged by
|
|
250
|
+
the exit evaluator instead of a command. It returns the same verdict vocabulary, so the
|
|
251
|
+
forbidden zone, F32 worker-crash routing (a check crashed by the worker's own broken test
|
|
252
|
+
feeds back — the F46 mechanism), and the cap taxonomy apply unchanged. `close` becomes
|
|
253
|
+
optional when `judge` is present; the seam is wired by `runPlan` only and is
|
|
254
|
+
inexpressible in any config or plan — the agent never authors its judge any more than
|
|
255
|
+
its close.
|
|
256
|
+
|
|
208
257
|
Escalations are decision-ready (category, options, spend); cap-halt is its own
|
|
209
258
|
category, never merged with "wrong". A thrown middle is relayed by its `category`
|
|
210
259
|
property (`cap-halt`, `gate-red`, …); an unnamed throw is `interpreter-red`. Close output
|
|
@@ -235,9 +284,35 @@ a leak on the very output it was guarding.
|
|
|
235
284
|
The operator-owned sibling (never an extension) of `validateConfig`: validates a
|
|
236
285
|
`job-v1` spec — see **All options** for the full schema, close types, and hierarchy.
|
|
237
286
|
Never throws on JSON text or plain parsed data (the ingest contract); returns the
|
|
238
|
-
parsed spec on ok, `null` on any red.
|
|
287
|
+
parsed spec on ok, `null` on any red. Validates BOTH job shapes (legacy `steps[]` and
|
|
288
|
+
the Layer 2 plan shape — see the option tables above). Menus exported:
|
|
239
289
|
`CLOSE_TYPES`, `CLASSES`, `CLASS_BY_CLOSE`, `GOLD_COMPARE`, `CADENCE_UNITS`,
|
|
240
|
-
`PROVIDERS`, `CONDITION_KEYS`, `STEP_MODES`, `TOOL_MENU`, `LOCKED_TOOLS
|
|
290
|
+
`PROVIDERS`, `CONDITION_KEYS`, `STEP_MODES`, `TOOL_MENU`, `LOCKED_TOOLS`,
|
|
291
|
+
`VERDICT_TYPES`, `LOCKED_VERDICTS`.
|
|
292
|
+
|
|
293
|
+
### `validatePlan(input, { job, maxStepRounds? })` → `{ ok, reds, plan }` — `src/plan.js`
|
|
294
|
+
|
|
295
|
+
The third validator: gates the AGENT-authored plan doc (`schema: "plan-v1"`) against the
|
|
296
|
+
SIGNED job spec before tokens burn — the ceiling, the fence, and the checks menu all come
|
|
297
|
+
from `job` (a missing or non-plan-shape job fails CLOSED, `job-invalid`). Never throws;
|
|
298
|
+
same `{ code, path, detail }` red shape as its siblings; `verb-escape` reds carry the
|
|
299
|
+
escaping verb as a structured `verb` field (the ledger keys on it). `maxStepRounds`
|
|
300
|
+
(default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds`.
|
|
301
|
+
Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS`.
|
|
302
|
+
|
|
303
|
+
### `snapshotScope(dir, scope)` / `evalExits(exits, { dir, snapshot?, runCheck? })` — `src/exits.js`
|
|
304
|
+
|
|
305
|
+
The shell's own fixed code for the closed exit menu — nothing here executes
|
|
306
|
+
agent-authored text. `snapshotScope` hashes every file under a scope prefix (the
|
|
307
|
+
"before" side of `tree-changed`; a missing dir snapshots empty). `evalExits` is AND-only
|
|
308
|
+
and never short-circuits — the result names EVERY failing wall (`{ pass, results }`,
|
|
309
|
+
each result `{ type, pass, detail?, fault? }`). `tree-changed` reads OUTCOME (bytes vs
|
|
310
|
+
the snapshot): an identical re-write is NOT a change (F43) and git status is never
|
|
311
|
+
consulted (F45). `artifact-written` rejects zero-byte files. `check-passes` delegates
|
|
312
|
+
through the `runCheck` seam (the runner wires runClose); an unwired or crashed seam
|
|
313
|
+
fails CLOSED with `fault` carrying a runClose verdict name — an instrument fault
|
|
314
|
+
escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details
|
|
315
|
+
are counts and names only, never file bodies (they ride the append-only spine).
|
|
241
316
|
|
|
242
317
|
### `jobSpecHash(job)` / `checkApproval(job, approvals)` — `src/job.js`
|
|
243
318
|
|
|
@@ -285,7 +360,32 @@ the close judges the tree; on-green `remember` retains the worker's change summa
|
|
|
285
360
|
Worker-caused close crashes feed back as `worker-crash` gaps via the gate audit (F32 —
|
|
286
361
|
see `ralph` above).
|
|
287
362
|
|
|
288
|
-
|
|
363
|
+
**Layer R — the within-run ratchet (`layerRoot`, default `false`; design record
|
|
364
|
+
2026-07-19).** The shell mechanically detects fixation across attempts — consecutive
|
|
365
|
+
attempts that rewrote the same file(s) without moving the close's kept-failure set
|
|
366
|
+
(`closeGapKeep` lines, duration stamps stripped before comparing; without a gapKeep the
|
|
367
|
+
detector degrades to write-overlap alone and the spine event names the mode) — and
|
|
368
|
+
injects an escalating note into the next prompt: first a capped summary naming the
|
|
369
|
+
repeated files, then (if still stuck) the worker's OWN prior failed edit content
|
|
370
|
+
verbatim, scrubbed and capped with trims announced. Inert unless stuck; the worker
|
|
371
|
+
authors nothing and gains no verb. Two axes, deliberately separate (F43): the DETECTOR
|
|
372
|
+
reads INTENT (the gate audit's allow-set — an edit whose anchor missed is still the
|
|
373
|
+
worker reaching for that file again, and a tree-diff detector measured blind to exactly
|
|
374
|
+
that), while the NOTE's claims about file contents read OUTCOME (settled post-execution
|
|
375
|
+
through `Loop`'s `onToolResult`). So a repeat that never applied is told its anchor
|
|
376
|
+
missed, and only content actually in the file is ever described as having landed. Spine event `root-injected` carries
|
|
377
|
+
`{stage, mode, streak, paths, redSetSize}` — counts and paths only, NEVER content (the
|
|
378
|
+
spine is append-only). State dies with the run: this is within-run scratch, not
|
|
379
|
+
across-run memory. `layerRoot: true` (on `interpret` and `runJob`) is the ON/experimental
|
|
380
|
+
arm; the default is OFF. Status (F41): the ratchet is MEASURED inert on every current
|
|
381
|
+
job — two frozen probes + an archive sweep read 0 fixated pairs in 14 (the F21 repetition
|
|
382
|
+
disease was a broken-loop symptom, since cured). Because ON has therefore never won its
|
|
383
|
+
own A/B, it ships **OFF by default (decided 2026-07-21)** — armed and correct, but not
|
|
384
|
+
default-enabled until a stuck job (Layer 2, or a manufactured-fixation probe) shows ON
|
|
385
|
+
beats OFF. Flip the default to `true` the day that evidence lands; a `root-injected` event
|
|
386
|
+
on your spine (when you pass `layerRoot: true`) is a signal worth reading, not noise.
|
|
387
|
+
|
|
388
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, emit, target?, capRuns?, shellCapUsd?, closeTimeoutMs?, execCmd?, layerRoot? })` → outcome — `src/run.js`
|
|
289
389
|
|
|
290
390
|
The N2 runner — the shell's top layer; composes everything below it and interprets
|
|
291
391
|
nothing itself. Sequence: **approval gate** (human-signs-always — refuses an unapproved
|
|
@@ -312,10 +412,61 @@ round the API cut off mid-generation (`truncated:max_tokens`, bare-agent 0.27.0/
|
|
|
312
412
|
before which it laundered into a clean finish, F25); in both cases no verdict exists and the
|
|
313
413
|
failed round's spend is only partly known (F6). `cap-halt` is drafting spend consuming the whole job budget before a
|
|
314
414
|
valid config existed. Both are decision-ready escalations with a terminal `job-end`:
|
|
315
|
-
the spine never dangles.
|
|
415
|
+
the spine never dangles. **Every `job-end` carries the money, on every path**:
|
|
416
|
+
`{ outcome, spentUsd, spendComplete }` (plus `step`/`cause`/`detail` where the outcome has
|
|
417
|
+
them). `spentUsd` is the accumulated sum of PRICED rounds ONLY — never an estimate from
|
|
418
|
+
token counts or averages (cap-not-estimate) — and it is stated even on the pre-token reds,
|
|
419
|
+
where the honest figure is a real `0`. `spendComplete` says whether that figure is EXACT:
|
|
420
|
+
`false` means one or more rounds came back unpriced (F6), so `spentUsd` is a FLOOR
|
|
421
|
+
("at least $X", true total unknowable) and must not be read as a total. Both fields are
|
|
422
|
+
present on all outcomes, so a consumer never branches on field presence and never has to
|
|
423
|
+
launder a missing `spentUsd` into `$0`. A text-mode job invoked without `opts.target` is a `job-red`
|
|
316
424
|
before ANY provider call (and `interpret` itself throws a TypeError for direct
|
|
317
425
|
callers) — reds-before-tokens applies to the call, not just the spec.
|
|
318
426
|
|
|
427
|
+
**The plan flow (Layer 2).** A plan-shape spec routes through the SAME `runJob` entry —
|
|
428
|
+
same approval gate, same smoke, same metered ledger, same job-end money contract
|
|
429
|
+
(`job-start` carries `shape: 'plan'` + the goal instead of a steps list; no `opts.target`
|
|
430
|
+
needed — plan steps are tool-mode by construction). The flow (`runPlan`, also exported
|
|
431
|
+
for direct callers who own their own ledger): **close precheck** (`already-green` is a
|
|
432
|
+
DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **checks
|
|
433
|
+
preflight** (every SIGNED check runs once at $0 — an unrunnable check is a `check-red`
|
|
434
|
+
stop before tokens, not a fault mid-plan) → **SCOUT** (read-only by construction: the
|
|
435
|
+
write verbs are not in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
|
|
436
|
+
the planner never sees the repo, only the scout blob; drafted against a schema
|
|
437
|
+
description with check NAMES only; `validatePlan` gates it, one redraft with the reds
|
|
438
|
+
fed back, then `plan-red`) → **EXECUTE** (strictly sequential micro-loops: `ralph` with
|
|
439
|
+
the exit-evaluator judge; tree snapshots at step start; the gap names every failing
|
|
440
|
+
wall — mechanical genre, F46's measured mechanism; artifacts feed forward labeled by
|
|
441
|
+
step id) → **ONE replan**, triggered by exhaustion only (an instrument stop never
|
|
442
|
+
replans) → **the operator's close**, a red feeding ONE bounded fix loop judged by the
|
|
443
|
+
REAL close. `plan-executed` (the plan-as-executed record, design law #2) lands on the
|
|
444
|
+
spine on every path that executed steps. Additional outcomes: `already-green |
|
|
445
|
+
plan-red | check-red | close-red`. Worker prompts hold the v1.12 §5 contract
|
|
446
|
+
(mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
|
|
447
|
+
the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
|
|
448
|
+
|
|
449
|
+
**Two worker surfaces — API and native clipipe (BA-16, module 4d).** The plan flow is
|
|
450
|
+
provider-agnostic: the close, the checks, and the exit evaluator are commands and form
|
|
451
|
+
checks, so ONLY the worker differs. `job.provider === 'clipipe-subscription'` drives tools
|
|
452
|
+
NATIVELY (the `claude` CLI owns the turn cycle) instead of through the `Loop`; every other
|
|
453
|
+
provider runs the Loop unchanged (byte-identical — the API path cannot regress). Native
|
|
454
|
+
governance is **constructor-time and per-worker**, so the runner cannot reuse one injected
|
|
455
|
+
instance: pass `opts.nativeProvider`, a FACTORY the runner calls fresh per worker as
|
|
456
|
+
`({ policy, onTurn?, maxTurns, hasTools }) => provider`. Return the right mode by `hasTools`:
|
|
457
|
+
`true` → native tool mode (`toolProtocol:'claude-mcp'`, wire the gate's `policy` + `onTurn` +
|
|
458
|
+
`maxTurns` onto the provider — the SAME `wireGate` fence, so a write outside the scope is
|
|
459
|
+
DENIED at the bridge, live-proven); `false` → the toolless drafter, where a native session
|
|
460
|
+
reports NO cost, so return a metered claude-json TEXT provider (`--output-format json`,
|
|
461
|
+
`parse:'claude-json'`) — never an unmetered, invisible-spend session. A missing factory on a
|
|
462
|
+
`clipipe-subscription` job is `interpreter-red`, never a silent fall-back to the metered API.
|
|
463
|
+
Money reconciles PER SESSION (the CLI prices the session, `costUsd` null per turn,
|
|
464
|
+
authoritative at close): the accounted `worker-round` is the session total, per-turn
|
|
465
|
+
`worker-turn` events are attribution only. `maxTurns` (→ CLI `--max-turns`) is the
|
|
466
|
+
per-attempt bound and surfaces `max_turns` as a BOUNDED attempt (judged, gap fed forward),
|
|
467
|
+
the native analog of `loop.stop()`. Live-validated green end-to-end on the real CLI; the
|
|
468
|
+
Loop path is untouched.
|
|
469
|
+
|
|
319
470
|
**Resume-to-cap (close-first skip):** every predicate step runs its close FIRST, before
|
|
320
471
|
any tokens (`close-precheck` on the spine, output scrubbed at capture like every close).
|
|
321
472
|
Already-green skips the step for zero tokens as a DISTINCT record — `step-end` outcome
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build:types"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"bare-agent": "^0.
|
|
26
|
+
"bare-agent": "^0.33.1",
|
|
27
27
|
"bareguard": "^0.12.0",
|
|
28
28
|
"litectx": "^0.29.1"
|
|
29
29
|
},
|