bareloop 0.1.0 → 0.3.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 +425 -2
- package/NOTICE +13 -0
- package/README.md +8 -6
- package/bareloop.context.md +334 -19
- package/package.json +4 -3
- package/src/extract.js +14 -6
- package/src/index.js +16 -2
- package/src/interpret.js +538 -46
- package/src/job.js +368 -0
- package/src/ledger.js +266 -0
- package/src/ralph.js +251 -14
- package/src/run.js +478 -0
- package/src/text.js +65 -8
- package/src/validate.js +175 -16
- package/types/extract.d.ts +4 -2
- package/types/index.d.ts +5 -2
- package/types/interpret.d.ts +44 -6
- package/types/job.d.ts +88 -0
- package/types/ledger.d.ts +76 -0
- package/types/ralph.d.ts +123 -17
- package/types/run.d.ts +58 -0
- package/types/text.d.ts +35 -1
- package/types/validate.d.ts +51 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,430 @@ 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.3.0] — 2026-07-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A `close.judged` pattern with more than one capture group now reds at validation
|
|
13
|
+
(F40).** `runClose` reads capture group 1 only, so an alternation carrying the count
|
|
14
|
+
in another branch left group 1 `undefined` → `NaN` → `judgedCount` null → an exit-0
|
|
15
|
+
**green stamped `crashed`** (the mirror of the fake green the floor exists to catch);
|
|
16
|
+
at precheck that escalates `close-crashed` before the worker runs. The validator's
|
|
17
|
+
message already promised ONE group and only enforced "not zero". Alternation stays
|
|
18
|
+
expressible with non-capturing branches: `(?:a|b) (\d+)`.
|
|
19
|
+
- **Upstream-ledger attribution reads a typed `lib` field instead of sniffing error
|
|
20
|
+
prose (F40).** `interpret` prefixes every worker-loop error with `worker loop:`, and
|
|
21
|
+
the verb sniff ran first — so a bare-agent transport failure whose text merely
|
|
22
|
+
contained "recall" was billed to litectx. The throw site now stamps the owner it
|
|
23
|
+
knows, `ralph` relays it, and prose remains the fallback for older spines (the
|
|
24
|
+
`request-red` contract).
|
|
25
|
+
- **An unrecognised escalation category is counted, not silently dropped (F40).** The
|
|
26
|
+
dispatch keyed on four bare literals with no default, so a new or renamed category
|
|
27
|
+
vanished exactly like a deliberate exclusion. Exclusions are now an executable set;
|
|
28
|
+
anything outside {classified} ∪ {excluded} is charged to bareloop as a stale mapping.
|
|
29
|
+
- **The drafter is offered a PER-STEP share of the budget, not the whole pot (F40).** The
|
|
30
|
+
config is drafted once and re-validated at every step against what is left, so a ceiling
|
|
31
|
+
sized to the whole budget went stale the moment step 1 spent: step 2 then red `bounds` on
|
|
32
|
+
an unchanged config with money still in the pot. The advertised ceiling is now
|
|
33
|
+
`(budgetUsd − drafting reserve) ÷ predicate steps`, which still fits after earlier steps
|
|
34
|
+
have spent (the shares sum to the reserve-less budget). A drafting bound, not an
|
|
35
|
+
enforcement one — cap-not-estimate is unchanged and still tested, and a step needing more
|
|
36
|
+
than its share cap-halts cleanly rather than starving the steps after it. **Every shipped
|
|
37
|
+
job has one predicate step and is arithmetically identical.**
|
|
38
|
+
- **`jobs/aurora-fix.json` counts tests EXECUTED, not tests PASSED (F40).** A passed-count
|
|
39
|
+
floor conflated "did the close judge?" with "did the tests pass?", so the red tree the job
|
|
40
|
+
exists to fix fell under the floor and escalated as an instrument crash at precheck,
|
|
41
|
+
before the worker ever ran. Pattern is now `collected (\d+) items`; the patient's
|
|
42
|
+
`close.sh` swaps `-q` for `-ra` (`-q` prints no executed-count line at all, and `-ra` is
|
|
43
|
+
required or the job's `gapKeep "^FAILED "` loses every line it carries to the worker).
|
|
44
|
+
- **Revisor rounds no longer spend the worker's per-attempt bound (F40).**
|
|
45
|
+
`roundsThisAttempt` resets once, *before* the revisor phase, and revisor turns share
|
|
46
|
+
the worker's metered handler — so R revisor rounds silently left the worker 40−R while
|
|
47
|
+
the prompt still advertised 40, and a revisor burning the bound stopped the worker loop
|
|
48
|
+
before its first tool call. Money still meters on the run's axis (F12); only the round
|
|
49
|
+
charge moved. Restores "the advertised bound and the enforced bound stay the same
|
|
50
|
+
numbers on both axes", and matches the carve-out the summarizer fold already had.
|
|
51
|
+
- **Two F6 cost launderings closed (F40).** `run-job1`/`run-job2` printed
|
|
52
|
+
`spent: $0.0000` for provider-red/pricing-red runs (whose `job-end` carries no
|
|
53
|
+
`spentUsd`, and which can end after real priced spend) — now `UNKNOWN`, the
|
|
54
|
+
`run-battery` spelling. The `revision-red`/`revision-accepted` spine events recorded
|
|
55
|
+
`costUsd: 0` for an unpriced revisor — now the honest null.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`scanSecrets(raw)` — the ONE spelling of the raw-text secret scan (F40).** The scan
|
|
60
|
+
was hand-rolled at seven call sites off `SECRET_PATTERNS`; detection and redaction
|
|
61
|
+
must never disagree about what a secret looks like. Exported beside the inventory,
|
|
62
|
+
`sweepSecretLiterals`'s text-side twin.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **Tool-mode attempt bound raised 24→40 rounds (F37).** The TESTGEN calibration curve
|
|
67
|
+
measured that no prompt condition (bound undisclosed / disclosed / disclosed+pacing
|
|
68
|
+
strategy) produced a graded one-shot at 24 rounds — the worker's read-first prelude
|
|
69
|
+
eats the window — while one run proved a form-passing suite fits when writing starts
|
|
70
|
+
by mid-attempt. The per-attempt cutoff and the Gate's run-wide `maxTurns` now derive
|
|
71
|
+
from ONE hoisted `TURNS_PER_ATTEMPT` constant (they must agree or the enforced bound
|
|
72
|
+
drifts from the advertised one).
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- **F32 — worker-crash attribution: a close crash the worker caused is a gap, not a stop.**
|
|
77
|
+
Battery pass 1 (F31) measured the gap: 4 of 7 rows whole-file-rewrote an orchestrator,
|
|
78
|
+
broke imports, the close crashed under the judged floor, and the run **escalated** — the
|
|
79
|
+
worker was never told "your edit crashed the suite", so no plant that needed a second
|
|
80
|
+
attempt ever got one. F17's forbidden zone was built against instrument crashes and could
|
|
81
|
+
not see worker-attributable ones. Now: a `crashed` verdict with worker writes on record
|
|
82
|
+
routes as the DISTINCT verdict **`worker-crash`** (spine event with the file list), the
|
|
83
|
+
gap tells the worker which files it wrote and to fix or revert, and the loop continues
|
|
84
|
+
under the same caps. Attribution instrument: the gate audit's allow-decision write/edit
|
|
85
|
+
lines, run_id-scoped, read through an injected `workerWrites` seam (`ralph` stays
|
|
86
|
+
stdlib-only and dumb). Escalation is UNCHANGED for true instrument crashes: crash at
|
|
87
|
+
precheck (structurally pre-worker) or crash with zero writes stays `close-crashed`, never
|
|
88
|
+
retried; an unreadable audit attributes nothing (fail toward the old behavior). Validated
|
|
89
|
+
against the real instrument (P3 rerun, sonnet, $0.77): all three crashes routed and fed
|
|
90
|
+
back, zero escalations-eaten rows, honest `cap-halt` stop — pass 1's same plant died at
|
|
91
|
+
attempt 1 with the worker never told. TDD, suite 292 → 299.
|
|
92
|
+
- **BA-13 consumed — the anchored edit verb (`bare-agent` 0.27.0 → 0.29.0).** `TOOL_MENU`
|
|
93
|
+
gains **`edit`** (job-spec grantable; `run` stays locked), `TOOL_BY_VERB` maps it to
|
|
94
|
+
`shell_edit` (anchored exact-once replace: BA-4 param guards, atomic rename, anchor-miss
|
|
95
|
+
as a refusal RESULT so the worker re-anchors). Judged by the SAME `writeScope` fence as
|
|
96
|
+
`write` (bareguard action type `'edit'`, already in its FS vocabulary); the F32
|
|
97
|
+
attribution instrument counts edit actions as worker writes; the tool-mode persona
|
|
98
|
+
carries the strategy (prefer the edit verb — F31: 4 of 5 big-file whole-writes broke the
|
|
99
|
+
tree). The frozen battery spec pins its grant explicitly, so the menu widening does NOT
|
|
100
|
+
change what the signed hash buys — granting `edit` to the battery is a new spec version.
|
|
101
|
+
Suite 299 → 303.
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **F33 — two verb-blind reporting instruments (battery pass 2's audit).** Pass 2 ran
|
|
106
|
+
7/7 attempt-1 green at $0.94 under the newly signed `edit`-granting spec — and the
|
|
107
|
+
battery's printed table said `writes=0` on a pass whose every fix was one anchored edit:
|
|
108
|
+
the collector counted only `write` actions, built before the `edit` verb existed and
|
|
109
|
+
never re-audited when the menu widened (F32's lesson, re-learned at the reporting layer
|
|
110
|
+
in the same session it was minted). `culpritRead` was equally blind to the retrieval
|
|
111
|
+
channels: P5 greened with the culprit's body handed to it by the drafted config's
|
|
112
|
+
`before-attempt` recall hook (`body: true` + litectx recency boost ranking the
|
|
113
|
+
just-planted chunk into the hits) — invisible to an instrument that only saw gated
|
|
114
|
+
reads. Fixed: the collector counts write AND edit as write-class; `culpritRead` sees
|
|
115
|
+
every read-class channel; both recall emit sites (`ctx-tool`, `hook-op`) now carry
|
|
116
|
+
`paths` so downstream instruments can see what reached the worker's context. The
|
|
117
|
+
archived pass-2 results JSON keeps its wrong zeros; FINDINGS F33 is the corrected read.
|
|
118
|
+
|
|
119
|
+
- **F28 — the gap bound cut every failure line out of the worker's feedback.** The first
|
|
120
|
+
real end-to-end firing of the N2 loop delivered a 1,927-char gap containing **zero**
|
|
121
|
+
`not ok` lines: `ralph`'s `boundGap` keeps a head sample + elided middle + tail, and a
|
|
122
|
+
large TAP suite (`npm test`, ~67KB) prints its failing tests in the *middle* — so the
|
|
123
|
+
worker was told "5 fail" and never *which*, three attempts running, and never navigated to
|
|
124
|
+
the culprit file. New optional **`close.gapKeep`** (job-spec, `predicate` only): a regex
|
|
125
|
+
**source** whose matching close-output lines are preserved in a capped, clearly-delimited
|
|
126
|
+
kept-failures block between head and tail — the failing-test NAMES reach the worker
|
|
127
|
+
regardless of where they print. Hard-capped (50 lines / 8192 bytes, whichever binds) so a
|
|
128
|
+
pathological close cannot rebuild the bloat; a trimmed block announces the trim (no silent
|
|
129
|
+
truncation). Validated like `judged.pattern` (must compile as a RegExp, else a spec red
|
|
130
|
+
before any tokens); **arbiter territory** — the drafted workflow config cannot express it.
|
|
131
|
+
Also fixed the adjacent hazard: the gap now combines **both** streams (stdout + stderr), so
|
|
132
|
+
a failure on stdout survives stderr noise (the old `err || out` returned stderr alone and
|
|
133
|
+
lost it). `jobs/mailproof-fix.json` gains `"gapKeep": "^not ok"` (spec re-signed). Threaded
|
|
134
|
+
spec → `runJob` → `interpret` → `ralph` parallel to `judged`. TDD, suite 281 → 291.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- **Consume `bare-agent@0.27.0` — the N2 build gate (BA-4) is cleared.** 0.27.0 ("Provider
|
|
139
|
+
Fidelity & Honest Termination") shipped the entire upstream ask queue this rung filed:
|
|
140
|
+
BA-4, BA-5, BA-3, BA-6, BA-7, BA-1 (+ BA-10/BA-12). **BA-4** (`shell_write` truncating a
|
|
141
|
+
file to zero bytes on absent `content`) was the hard N2-exit blocker — its four acceptance
|
|
142
|
+
criteria are re-verified locally against the published tarball, so a write-granting tool
|
|
143
|
+
mode can ship honestly. The tool-mode middle (`src/interpret.js`) is updated to the new
|
|
144
|
+
contract: a **`truncated:max_tokens`** round (BA-6 — a round the API cut off, previously
|
|
145
|
+
laundered into a clean `error:null` finish, F25) now escalates as **provider-red** (retry,
|
|
146
|
+
the F11 transport class) instead of being scored as an empty attempt; **`loop.stop()`**'s
|
|
147
|
+
new `error:null`+text return (BA-3/BA-5) let the `stoppedByBound` shim be **deleted** (it
|
|
148
|
+
was dead under the new contract and could have swallowed a genuine halt); and
|
|
149
|
+
**`cacheMessages: true`** (BA-1) is wired on the worker loop — the transcript cache the
|
|
150
|
+
job #1 cost wall (F18: 754k full-price tokens, died at cap) needed. Regression test for
|
|
151
|
+
the truncation path added (mutation-checked). Suite 280/280, typecheck clean.
|
|
152
|
+
|
|
153
|
+
- **Agent/IDE scratch gitignored and de-tracked.** `.gitignore` now default-denies every dot-directory (`.*/`), re-admitting only what ships (`.github/`). Per-machine agent/IDE state (`.claude/`, `.litectx/`, `.idea/`, …) regenerates locally and only added noise and churn; any already-committed copies are removed from tracking (local files kept on disk). Repo hygiene only.
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
- **N2 — the headless single-job loop (rung 3 of the ladder), modules 1–4 + 2b.**
|
|
157
|
+
- **`runJob(spec, opts)`** (`src/run.js`): the runner — approval gate (human-signs-always,
|
|
158
|
+
refuses before ANY token: `unapproved-spec`) → litectx known-answer smoke before tokens
|
|
159
|
+
(`smoke-red`, adaptlearn A3) → config drafting through the PRICED path, one sealed shot
|
|
160
|
+
+ one redraft with reds fed back (`config-red` on a second red, zero grinding) →
|
|
161
|
+
sequential per-step interpret loops under the ONE cumulative ledger (a step-red stops
|
|
162
|
+
the job with attribution: `step-red:<id>`) → the hitl step opens a **draft PR
|
|
163
|
+
deterministically** (branch → stage the job fence ONLY → commit → push →
|
|
164
|
+
`gh pr create --draft`, through an injectable shell-owned exec seam — model tools never
|
|
165
|
+
touch git; a failure is `pr-red` + the escalation still fires) and ends `escalated` by
|
|
166
|
+
design, the PR URL riding the decision-ready escalation. New spine vocabulary:
|
|
167
|
+
`job-start/end`, `step-start/end`, `primitive-smoke`, `draft-result`, `pr-opened`,
|
|
168
|
+
`pr-red`.
|
|
169
|
+
- **Tool-mode middle** (module 2b): `job-v1` steps gain `mode: "text"|"tools"` and
|
|
170
|
+
`tools` (unique subset of `read|grep|write` — `TOOL_MENU`, frozen; requesting `run`
|
|
171
|
+
reds: locked-but-listed, admission waits on request-red evidence). The SPEC owns the
|
|
172
|
+
grant; the drafted config cannot express either. In tool mode the worker drives
|
|
173
|
+
bare-agent's shell tools with every call policy-checked against the same fence
|
|
174
|
+
(`actionTranslator` maps tool calls onto write/read actions; paths resolve exactly as
|
|
175
|
+
the tools resolve them), reads pinned to the workdir (`readScope`), a denial streak
|
|
176
|
+
stopping as `gate-red`. `STEP_MODES`/`TOOL_MENU` exported.
|
|
177
|
+
- **`artifact-red` + fence-robust extraction** (module 3): ONE parser (`extractArtifact`)
|
|
178
|
+
for every model-output parse — prose-wrapped and mid-text fences extract clean; a
|
|
179
|
+
non-artifact response reds on its OWN axis, writes nothing, and the retry is told why
|
|
180
|
+
(non-terminal, under ralph's cap). Text mode only — in tool mode the tools write
|
|
181
|
+
directly and the close judges the tree (there is no response artifact to red).
|
|
182
|
+
- **ralph options** (module 1): `closeTimeoutMs` (close wall-clock cap, was hardcoded
|
|
183
|
+
120s) and the tail-biased gap bound (400 head + 1500 tail — the assertion diff lives
|
|
184
|
+
at the end; head-only truncation fed the worker pure preamble).
|
|
185
|
+
- **The upstream ledger** (module 4): `updateLedger({ledgerFile, spineFiles})`
|
|
186
|
+
(`src/ledger.js`) folds run spines into ONE append-only incident JSONL — the
|
|
187
|
+
A1/A2/A3 upstream-ask flow, mechanized: evidence in, human judgment out
|
|
188
|
+
(`suggestedAsk` is a template seed, never an auto-file; status rows
|
|
189
|
+
`filed → fixed → consumed` stay human-appended). Keys `lib:verb:class:sig` dedupe
|
|
190
|
+
the same bug across runs (short hash of the path/number-normalized detail); rows
|
|
191
|
+
are cumulative deltas, the fold is current state, and the collector is idempotent
|
|
192
|
+
over the same corpus — the ledger is derived and reconstructible, spines stay
|
|
193
|
+
ground truth. Classes worst-first (`LEDGER_CLASSES`, frozen): `silent-degradation`,
|
|
194
|
+
`runtime-red`, `provider-red`, `pricing-red` (added vs the design doc — F6),
|
|
195
|
+
`capability-gap` (ships dormant until in-loop admission), `broken-close`,
|
|
196
|
+
`request-red`, `retention-red`, `config-red` (attributed to bareloop's own
|
|
197
|
+
drafting schema). Excluded by doctrine: bare `cap-halt` (budget story),
|
|
198
|
+
`close-verdict`/`artifact-red` (worker stories), `gate-red` (governance working
|
|
199
|
+
as intended), `pr-red` (operator environment). Pure pieces exported:
|
|
200
|
+
`classifyIncidents`, `foldLedger`, `ledgerDeltas`. Design record:
|
|
201
|
+
`docs/plans/2026-07-11-upstream-ledger-design.md` + 2026-07-13 addendum (the
|
|
202
|
+
bareloop event mapping). CLI lands at N5; the panel reads the same file at N6.
|
|
203
|
+
- **Resume-to-cap: close-first skip** (module 4.5): every predicate step runs its
|
|
204
|
+
close BEFORE any tokens (`close-precheck` on the spine, output scrubbed at capture
|
|
205
|
+
like every close). Already-green skips the step for zero tokens as a DISTINCT
|
|
206
|
+
record (`step-end` outcome `already-green`, never plain `green` — nothing was done,
|
|
207
|
+
so it mints no learning credit and runs no on-green retention); a close that cannot
|
|
208
|
+
RUN stops `broken-close` before any provider call. Config drafting is deferred to
|
|
209
|
+
the first step that actually needs a worker (still one sealed shot + one redraft,
|
|
210
|
+
always drafted fresh per run) — so the resume story is: a `cap-halt` stop is the
|
|
211
|
+
checkpoint (the workdir + the closes), the human raises `budgetUsd` (new spec hash,
|
|
212
|
+
re-sign) and reruns, finished steps skip in seconds, and a clean cadenced rerun
|
|
213
|
+
costs ZERO provider calls. Design record: the 2026-07-13 addendum on
|
|
214
|
+
`docs/plans/2026-07-12-n2-headless-loop-design.md`.
|
|
215
|
+
- **The cadenced no-op is silent (F7).** A `hitl` step now checks the fence
|
|
216
|
+
(`git status --porcelain -- <fence>`) before touching git: an affirmatively-clean
|
|
217
|
+
fence emits `pr-skipped` + `step-end: already-green` and the job ends **green** —
|
|
218
|
+
no PR, no escalation. (Before: a green-repo cadence opened a branch, and `git
|
|
219
|
+
commit` correctly failed "nothing added to commit" → a broken-PR escalation every
|
|
220
|
+
single day. A hitl close is a human decision point; with no changes there is no
|
|
221
|
+
decision.) A FAILED check — not a repo, broken git — falls through to the PR path
|
|
222
|
+
and reds honestly: an unknown fence state is never a green.
|
|
223
|
+
- **The PR step hands the checkout back (F7).** The starting branch is read before
|
|
224
|
+
anything moves and restored on every path, success or failure; a restore that fails
|
|
225
|
+
is a loud `workdir-red` naming the stranded branch, and never un-opens a real PR.
|
|
226
|
+
(Before: the workdir was left on `bareloop/<job>-<id>`, so the next cadenced run
|
|
227
|
+
branched off the previous run's unmerged branch and judged its close against that
|
|
228
|
+
state.) New spine vocabulary: `close-precheck`, `pr-skipped`, `workdir-red`;
|
|
229
|
+
`step-end` gains the `already-green` outcome.
|
|
230
|
+
|
|
231
|
+
- **Nine defects found by the first REAL-MODEL runs of job #1 (F8–F16)** — all fixed
|
|
232
|
+
TDD-first, all invisible to a stubbed seam:
|
|
233
|
+
- **`cwd` (F8):** `runClose` spawned the close with NO cwd, so a cwd-relative close
|
|
234
|
+
(`npm test`) ran in the RUNNER's directory — the arbiter judged the wrong repository.
|
|
235
|
+
`cwd` now threads runner → `ralph` → `spawnSync` (`ralph({cwd})`, `runClose(…, {cwd})`).
|
|
236
|
+
- **Drafting ceiling (F9):** the prompt advertised the JOB budget while the validator
|
|
237
|
+
enforced budget − drafting-spend — a bound the drafter was never told, so every real run
|
|
238
|
+
(the model claims the ceiling it is given) deadlocked `config-red`. The shell now reserves
|
|
239
|
+
its own drafting allowance and advertises `budget − reserve`: one number, advertised and
|
|
240
|
+
enforced.
|
|
241
|
+
- **Repository root (F10):** tool mode now tells the worker the absolute workdir. bare-agent's
|
|
242
|
+
shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
|
|
243
|
+
blind — the real one groped `/home/…`, the runner's dir, then `/`, and the fence denied it.
|
|
244
|
+
- **`provider-red` in the worker path (F11):** a transport throw out of `loop.run()` (the real
|
|
245
|
+
run: `read ENETUNREACH`) was filed `interpreter-red` ("fix the middle"). It is a provider
|
|
246
|
+
failure — retry, don't debug.
|
|
247
|
+
- **Per-round metering (F12):** the ledger accounted `worker-result`, emitted only after
|
|
248
|
+
`loop.run()` RETURNS — so an attempt that HALTS reported nothing: the real run spent $1.4375
|
|
249
|
+
and the ledger said $0.0048. Money is now metered per ROUND (`worker-round`, at
|
|
250
|
+
`onLlmResult`), including the round that trips the cap. Unpriced is never free (F6).
|
|
251
|
+
- **The close's current output (F13):** the precheck's gap now reaches the first attempt as the
|
|
252
|
+
tree's state (never as "your previous attempt"). The `run` verb is locked, so without it the
|
|
253
|
+
worker cannot see the failure it was hired to fix.
|
|
254
|
+
- **The arbiter's books (F14):** tool mode denies reads of `gate-audit.jsonl`, `.smoke` and
|
|
255
|
+
`.litectx` — the real worker read its own gate audit and spine. The agent does not author its
|
|
256
|
+
arbiter, and does not read its records.
|
|
257
|
+
- **The loop contract (F16):** the tool persona now tells the worker it is ONE attempt inside
|
|
258
|
+
`while close-red and under-cap` and will be re-run with the close's verdict. Without it a
|
|
259
|
+
model one-shots — the real run read for 12 rounds, never wrote, and ate the whole budget.
|
|
260
|
+
- **Job #1's close (F15):** `node --test --test-reporter=dot`, not `npm test` — the tail-biased
|
|
261
|
+
gap bound buries mid-stream failures in a 391-test TAP stream, so the worker was told "3
|
|
262
|
+
failed" and nothing else. A close's output format is part of its contract with the worker.
|
|
263
|
+
|
|
264
|
+
### Changed
|
|
265
|
+
- **`job-v1`: requesting a locked tool is now a DISTINCT red.** `tools` containing
|
|
266
|
+
`run` reds with code `request-red` (was a generic `invalid-value`) so the ledger can
|
|
267
|
+
tally admission demand — a generic code buried the evidence as a typo. An unknown
|
|
268
|
+
tool name stays `invalid-value`. `LOCKED_TOOLS` (frozen, `['run']`) exported.
|
|
269
|
+
- **`interpret` opts:** `target` is now optional (required in text mode only); new
|
|
270
|
+
`mode`/`tools` opts thread the spec's grant. Additive — existing callers unchanged.
|
|
271
|
+
- **Cost contract:** `extractRules` returns `costUsd: number|null` — null is the honest
|
|
272
|
+
"spend unknown" (F6); callers must not coerce it to 0.
|
|
273
|
+
|
|
274
|
+
### Fixed
|
|
275
|
+
- **Review round 2026-07-13 (8 confirmed findings, all execution-verified):**
|
|
276
|
+
- **extractArtifact wrapper-vs-content gate:** a fence counts as the artifact's
|
|
277
|
+
wrapper only when it opens within the first 5 lines of the response; a fence
|
|
278
|
+
buried deeper inside an unfenced reply is the artifact's OWN content and the
|
|
279
|
+
whole reply is the artifact, verbatim. Before: an unfenced doc-generator module
|
|
280
|
+
containing a ```js example``` was silently truncated to the 2-line fragment
|
|
281
|
+
with `red: null`, corrupting the close signal. Trade-off pinned in tests:
|
|
282
|
+
past the window, prose + fence is treated as the artifact (rare under the
|
|
283
|
+
no-fences persona); fence-heavy artifacts belong in tool mode.
|
|
284
|
+
- **Secret-leak channel closed:** `openDraftPr` now scrubs git/gh subprocess output
|
|
285
|
+
with the ONE shape inventory (`SECRET_PATTERNS`) at capture — a credentialed
|
|
286
|
+
remote URL echoed by a failed `git push` never reaches `pr-red`/the escalation's
|
|
287
|
+
`pr.error` on the append-only spine (same doctrine as the close path).
|
|
288
|
+
- **Plan-shape spend is metered:** the job ledger accounts `worker-plan` events too
|
|
289
|
+
(a separate `loop.run` whose metrics never fold into the implement call's) —
|
|
290
|
+
plan calls now drain `spentUsd` and an unpriced plan call halts `pricing-red`.
|
|
291
|
+
- **The spine never dangles:** a provider transport throw during drafting is a
|
|
292
|
+
decision-ready **`provider-red`** terminal (new outcome + escalation category,
|
|
293
|
+
classified by the ledger); an interpreter throw outside the loop (e.g. a broken
|
|
294
|
+
gate audit path) escalates `interpreter-red` with a terminal `job-end`.
|
|
295
|
+
- **Reds-before-tokens for the call, not just the spec:** a text-mode job invoked
|
|
296
|
+
without `opts.target` is a `job-red` before ANY provider call (`interpret`
|
|
297
|
+
throws a loud TypeError for direct callers); previously it burned a draft + a
|
|
298
|
+
worker call, the gate default-allowed the absent path, and `writeFileSync`
|
|
299
|
+
crashed as a misfiled interpreter-red.
|
|
300
|
+
- **Whitespace-padded `close.cmd` reds** (`invalid-value`) — a leading space made
|
|
301
|
+
`spawnSync('')` throw synchronously past every belt; the runner also trims
|
|
302
|
+
before splitting (defense in depth). **`cap-halt` job outcome:** drafting spend
|
|
303
|
+
that consumes the whole budget stops honestly (no paid redraft over a blown
|
|
304
|
+
budget, no config-red blaming the drafter).
|
|
305
|
+
- **One instrument for the F6 cost read:** `priceOf(result)` (`src/text.js`)
|
|
306
|
+
replaces four hand-copied `metrics ? costUsd : (cost ?? null)` spellings;
|
|
307
|
+
`REMEMBER_KINDS` is exported from the validator so the drafting prompt
|
|
308
|
+
advertises the menu the validator enforces (no drift). `request-red` reds carry
|
|
309
|
+
the locked verb as a **structured `verb` field** — the ledger keys on it (prose
|
|
310
|
+
stays a legacy fallback). Stale "clamped by validation" JSDoc corrected (the
|
|
311
|
+
validator REDS bounds; it never clamps).
|
|
312
|
+
- **Three silent $0 cost launderings (the F6 class) in shipped code:** `interpret`'s
|
|
313
|
+
worker cost emit (`?? cost` chain), `extract.js`'s rules-path cost (`?? cost ?? 0`),
|
|
314
|
+
and `extract.js`'s transport-throw path reporting unmeasured spend as `$0`. All now
|
|
315
|
+
carry the honest null + `unpricedRounds`; `runJob` halts `pricing-red` on either
|
|
316
|
+
signal (unpriced is never free — F6, PRD v1.8).
|
|
317
|
+
|
|
318
|
+
### Fixed (release review, 2026-07-19 — fresh full gates over the whole branch)
|
|
319
|
+
|
|
320
|
+
- **The plan-only call no longer carries the tool menu.** In tool mode with a drafted
|
|
321
|
+
`loop.shape: 'plan'`, the decompose call ("Plan only, no code") was offered the full
|
|
322
|
+
granted menu — a model calling `shell_write` during the plan round mutates the tree
|
|
323
|
+
before the implement round exists. The menu IS the grant (2b): the plan call now gets
|
|
324
|
+
an empty menu. Reachable by any tool-mode job on any run (the drafter picks the shape);
|
|
325
|
+
the combination was unexercised by any test until now.
|
|
326
|
+
- **`extractRules` consumes the parser's own red.** It took `extractArtifact(...).code`
|
|
327
|
+
and dropped `.red`, so an empty model response surfaced as a generic JSON
|
|
328
|
+
`parse-error` instead of the already-computed `'empty response'` — the ONE-parser
|
|
329
|
+
doctrine requires both callers to consume the red field (`interpret` already did).
|
|
330
|
+
Now a distinct `artifact-red`.
|
|
331
|
+
- **`run-job1` couples `shellCapUsd` to `spec.budgetUsd`** like every sibling runner —
|
|
332
|
+
the library default cap of $2 was a second, silent ceiling: a signed resume top-up
|
|
333
|
+
above $2 would red `bounds` on a budget the human explicitly approved (the advertised
|
|
334
|
+
budget must BE the enforced one).
|
|
335
|
+
- **`ctx_get`'s repo-relative conversion is boundary-aware** (`workdir + sep`): a bare
|
|
336
|
+
prefix match would garble a sibling path like `<workdir>-backup/x`. Defense in depth —
|
|
337
|
+
the gate independently denies such paths before the tool executes.
|
|
338
|
+
- **`jobs/litectx-maintainer.json`: `gapKeep "^✖ "` + the `edit` grant** — the two
|
|
339
|
+
omissions vs every sibling spec. The keep pattern is derived from the real
|
|
340
|
+
`--test-reporter=spec` red output (failing tests repeat unindented in the summary
|
|
341
|
+
block, so each failing-test NAME survives the gap bound exactly once — F28). Spec
|
|
342
|
+
edit = new hash; `run-job1` refuses until re-approved.
|
|
343
|
+
- **`scanSecrets` + `CLOSE_FAULTS` are exported from the package root.**
|
|
344
|
+
`bareloop.context.md` documented both as public API; the exports map admits only
|
|
345
|
+
`"."`, so neither was actually reachable by an adopter. The contract is now true.
|
|
346
|
+
- **Recorded, parked (arbiter territory):** symlink write-through is bareloop's
|
|
347
|
+
caller-contract debt — bareguard documents that its fence resolves lexical traversal
|
|
348
|
+
only and callers must canonicalise. No granted verb can create a symlink; the vector
|
|
349
|
+
needs a pre-existing one inside the patient's writeScope. UPSTREAM-ASKS "OUR SIDE" §7.
|
|
350
|
+
|
|
351
|
+
## [0.2.0] — 2026-07-12
|
|
352
|
+
|
|
353
|
+
### Added
|
|
354
|
+
- **N1 — the job/close schema (rung 2 of the ladder).** `validateJob` (`src/job.js`):
|
|
355
|
+
the operator-owned `job-v1` spec — the arbiter's rulebook as pure declarative data
|
|
356
|
+
(close chain, budget, outer write fence, environment label, escalation), validated
|
|
357
|
+
reds-before-tokens with pinned `code:path` reds. The arbiter split is guarded from
|
|
358
|
+
both sides by inexpressibility (workflow config can't say `close`/`provider`; job spec
|
|
359
|
+
can't say `hooks`/`loop`/`memory`, minting claims, or the shell-owned retry cap).
|
|
360
|
+
Close-authoring hierarchy (PRD §7) enforced as a class menu keyed by close type —
|
|
361
|
+
verdict-class laundering (`rubric` claiming `hard`) is a named red `close-hierarchy`.
|
|
362
|
+
`jobSpecHash` + `checkApproval`: the pure half of human-signs-always (sha256 over
|
|
363
|
+
canonical JSON; an edited spec is unapproved by construction; the N2 runner enforces).
|
|
364
|
+
Design record: `docs/plans/2026-07-12-n1-job-close-schema-design.md`; POC verdict: F4.
|
|
365
|
+
- **Two-layer write fence.** `validateConfig` accepts `jobWriteScope` (the job spec's
|
|
366
|
+
operator-owned outer fence); every workflow scope must fit inside it — path-boundary
|
|
367
|
+
aware (`src2` is not inside `src`) — or it reds `scope-escape`. Same containment law,
|
|
368
|
+
same code, both layers (the F9 lesson).
|
|
369
|
+
- **Reserved spine vocabulary: `coordination-red`** (V7, PRD v1.7 #1) — documented in
|
|
370
|
+
`bareloop.context.md`; no machinery until job #1 surfaces one.
|
|
371
|
+
|
|
372
|
+
### Changed
|
|
373
|
+
- **`validateConfig` returns `{ ok, reds, config }`** — the parsed config on ok, `null`
|
|
374
|
+
on any red; kills the interpreter's double-parse (N2+ queue item absorbed). Additive
|
|
375
|
+
for callers reading `ok`/`reds`.
|
|
376
|
+
|
|
377
|
+
### Fixed
|
|
378
|
+
- **Review hardening (post-build /code-review, 8 findings fixed + 6 sub-cap cleanups;
|
|
379
|
+
all fixes negative-tested and mutation-checked, zero feature regressions):**
|
|
380
|
+
cadence/escalation red unknown keys (the last smuggling level in a signed spec is
|
|
381
|
+
closed); the `jobWriteScope` fence opt fails CLOSED — a malformed fence is its own
|
|
382
|
+
`fence-invalid` red, never silently skipped, and each escaping scope reds at its own
|
|
383
|
+
indexed path (`gate.writeScope.N`); scope normalization moved into the shared
|
|
384
|
+
`globToPrefix` (leading `./`, interior `/./`, `//`, trailing `/` collapse) so a
|
|
385
|
+
validateJob-green fence like `src/` no longer deadlocks contained workflow configs;
|
|
386
|
+
`canon()` follows JSON semantics (undefined-valued keys dropped) so approvals survive
|
|
387
|
+
a disk round-trip, and `checkApproval` never throws (non-JSON spec → `false`);
|
|
388
|
+
`SECRET_RE` gained a left boundary (`flask-sqlalchemy` no longer reds) and the sweep
|
|
389
|
+
is shared by BOTH validators — the agent-authored workflow config is now swept too;
|
|
390
|
+
`interpret` accepts `jobWriteScope` and enforces the fence at the choke point (entry
|
|
391
|
+
+ revision candidates); revision candidates are judged and installed on their PARSED
|
|
392
|
+
form (a JSON-string candidate no longer false-reds arbiter-touch); exported arbiter
|
|
393
|
+
menus are frozen; `isObj`/`isNonEmptyString` single-copied in `validate.js`.
|
|
394
|
+
- **Second-round review (self-review of the hardening commit found a regression IT
|
|
395
|
+
introduced — all fixes TDD'd, mutation-checked, zero feature regressions):**
|
|
396
|
+
**critical containment escape** — the fence-normalization added to `globToPrefix`
|
|
397
|
+
stripped a leading `./` before collapsing `//`, so `.//src/**` minted the ABSOLUTE
|
|
398
|
+
prefix `/src`, validated green, and resolved outside the run directory at enforcement
|
|
399
|
+
(design law #1); fixed by collapsing `//`+`/./` first (so `.//src/**` → `src`, safe),
|
|
400
|
+
a belt in `scopeContained` rejecting any normalized-absolute prefix, and an enforcement
|
|
401
|
+
belt in `interpret` that refuses to build a Gate whose resolved scope escapes the
|
|
402
|
+
workdir. `canon()` now honors `toJSON` (a `Date` no longer hashes as `{}`; distinct
|
|
403
|
+
values no longer collide) and `jobSpecHash` never throws (the minting path the runner
|
|
404
|
+
calls directly is now crash-free on `BigInt`/cycles). `SECRET_RE` left boundary extended
|
|
405
|
+
to `-`/`_` (`pipeline-sk-transform-utils-v2` no longer false-reds; real keys still red).
|
|
406
|
+
`jobWriteScope: null`/`undefined` are the legitimate no-fence spellings (no more deadlock
|
|
407
|
+
on every config); a malformed fence reds `fence-invalid` at path `jobWriteScope`, not the
|
|
408
|
+
innocent workflow field (no ledger misattribution), with the detail bounded. Shared
|
|
409
|
+
`legalScopeEntry` gives the scope-legality law one home across all three call sites.
|
|
410
|
+
- **Secrets never enter the spine (hard line), enforced at the source.** `runClose`
|
|
411
|
+
(`src/ralph.js`) scrubs close-command output the moment it is captured, so a secret a
|
|
412
|
+
checked command echoes (a 401 dumping a `Bearer …`/`sk-…` header) never reaches the
|
|
413
|
+
append-only spine or the next worker prompt. The redactor is injected (the shell stays
|
|
414
|
+
stdlib-only); `interpret` wires bareguard's exported `redact`. A benign gap is returned
|
|
415
|
+
byte-identical — the failure still reaches the human, just without the token (design
|
|
416
|
+
law #7 / V4 intact: the redactor is a fixed shell primitive, not an emergent component).
|
|
417
|
+
- `NOTICE` ships in the tarball (npm auto-includes LICENSE/README but not NOTICE; Apache-2.0
|
|
418
|
+
wants both) — found validating the installed 0.1.0 artifact.
|
|
419
|
+
- **Release-gate review (fresh `/security` + `/diff-review` on the whole release diff;
|
|
420
|
+
every finding execution-verified, every fix TDD'd):** the spine redactor now scrubs
|
|
421
|
+
**every shape the validator reds** — bareguard's defaults cover only `Bearer`/`sk-`,
|
|
422
|
+
so a git close echoing a `ghp_`/`github_pat_`/`AKIA`/`xox` token passed unredacted
|
|
423
|
+
into the append-only spine (the most plausible leak for job #1, a GitHub PR workflow);
|
|
424
|
+
`SECRET_PATTERNS` is now the one shape inventory shared by detection and redaction.
|
|
425
|
+
`interpret` normalizes `workdir` once at entry — a trailing-slash or relative spelling
|
|
426
|
+
made the enforcement belt false-red every legal scope — and the belt now treats a scope
|
|
427
|
+
resolving to the run dir itself as escaped. `checkApproval` no longer routes through the
|
|
428
|
+
un-hashable sentinel (two distinct un-hashable specs cross-approved each other; now
|
|
429
|
+
un-hashable = unapproved). The secrets sweep tests object **keys**, not just values (a
|
|
430
|
+
token could ride a key in a `gold` `expected` onto the spine through a green spec). A
|
|
431
|
+
non-object `cadence` reds once at `cadence`, not twice at paths that don't exist.
|
|
9
432
|
|
|
10
433
|
## [0.1.0] — 2026-07-11
|
|
11
434
|
|
|
@@ -119,6 +542,6 @@ feature lands, **patch** = docs, fixes, scaffolding.
|
|
|
119
542
|
`docs/UPSTREAM-ASKS.md`, guardrails pre-tool hook (local), `.gitignore`.
|
|
120
543
|
- Public GitHub repo `hamr0/bareloop`, `main` branch.
|
|
121
544
|
|
|
122
|
-
[
|
|
545
|
+
[0.2.0]: https://github.com/hamr0/bareloop/compare/v0.1.0...v0.2.0
|
|
123
546
|
[0.1.0]: https://github.com/hamr0/bareloop/compare/v0.0.1...v0.1.0
|
|
124
547
|
[0.0.1]: https://github.com/hamr0/bareloop/releases/tag/v0.0.1
|
package/NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
bareloop
|
|
2
|
+
Copyright 2026 hamr0
|
|
3
|
+
|
|
4
|
+
This product includes software developed by hamr0
|
|
5
|
+
(https://github.com/hamr0/bareloop).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License").
|
|
8
|
+
You may obtain a copy of the License at:
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
This NOTICE file is required by Section 4(d) of the License.
|
|
13
|
+
Forks and derivative works MUST preserve this attribution.
|
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-N2%20%C2%B7%20headless%20loop-2a6d3a" alt="status: N2, headless loop">
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
**"Automate this job — I don't know the best workflow."** For tasks that are **repeated,
|
|
@@ -25,8 +25,10 @@ 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
|
-
>
|
|
28
|
+
> **Status: N2 — the headless loop is real.** `runJob()` takes a signed job spec through
|
|
29
|
+
> draft → per-step worker loops → close verdicts → draft PR, under one budget the agent
|
|
30
|
+
> can only tighten. Rungs N0–N2 have landed (roadmap below); next is executed
|
|
31
|
+
> inheritance (N3), where the ladder's kill-switch question gets its answer.
|
|
30
32
|
|
|
31
33
|
## Quick start
|
|
32
34
|
|
|
@@ -93,9 +95,9 @@ ladder, and the stop is a result.
|
|
|
93
95
|
|
|
94
96
|
| Rung | What lands |
|
|
95
97
|
|---|---|
|
|
96
|
-
| **N0** | Port + outer shell + spine (token-free) |
|
|
97
|
-
| **N1** | Job/close schema + validator |
|
|
98
|
-
| **N2** | Single-job headless loop — job #1 minimal (review→fix→PR, hard greens only) |
|
|
98
|
+
| **N0** ✅ | Port + outer shell + spine (token-free) — v0.1.0 |
|
|
99
|
+
| **N1** ✅ | Job/close schema + validator — v0.2.0 |
|
|
100
|
+
| **N2** ✅ | Single-job headless loop — job #1 minimal (review→fix→PR, hard greens only) — v0.3.0 |
|
|
99
101
|
| **N3** | Executed inheritance + contrast-bit extractor — **kill-switch: rules must transmit across non-identical runs** |
|
|
100
102
|
| **N4** | Verdict classes complete (soft/HITL minting) |
|
|
101
103
|
| **N5** | Scheduler + budget ops |
|