bareloop 0.12.0 → 0.13.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 +93 -0
- package/bareloop.context.md +106 -2
- package/package.json +1 -1
- package/src/index.js +16 -1
- package/src/ledger.js +102 -0
- package/src/plan.js +43 -2
- package/src/planrun.js +167 -14
- package/src/readshim.js +538 -0
- package/src/reuse.js +3 -1
- package/src/run.js +24 -2
- package/src/tools.js +34 -2
- package/types/index.d.ts +2 -1
- package/types/ledger.d.ts +58 -0
- package/types/plan.d.ts +19 -2
- package/types/planrun.d.ts +37 -2
- package/types/readshim.d.ts +117 -0
- package/types/reuse.d.ts +2 -0
- package/types/run.d.ts +15 -1
- package/types/tools.d.ts +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,99 @@ 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
|
+
## [0.13.0] — 2026-08-23
|
|
9
|
+
|
|
10
|
+
### Fixed (2026-08-23 — the release round: a pointer that lied, a close with no home, a rule that charged for its own refusal)
|
|
11
|
+
|
|
12
|
+
- **The pointer no longer lies about a file emptied since it was read.** `src/readshim.js`'s
|
|
13
|
+
pointer branch tested `start >= total` alone, so `0 >= 0` held with **no ledger entry at all** —
|
|
14
|
+
the first read of an empty file and a re-read of a file TRUNCATED TO EMPTY since delivery were
|
|
15
|
+
indistinguishable, and both answered "you already hold all 0 bytes". A worker would have gone on
|
|
16
|
+
believing in content that was gone. The guard now requires a ledger entry whose hash still
|
|
17
|
+
matches. This is precisely the untruthful-pointer class the module exists to prevent, found
|
|
18
|
+
inside the module built to prevent it, and reproduced live before the fix.
|
|
19
|
+
- **`judge-round` carries its rate provenance.** The judged floor made it the first spend a CLOSE
|
|
20
|
+
has ever had, and `src/ledger.js` documented the write side as forwarding `rateSource` onto it —
|
|
21
|
+
but the emit never spread the field, so every judge round would have read UNKNOWN forever. NOT
|
|
22
|
+
rounded up: `src/kinds.js`'s `onJudgeCost` payload still does not carry the field, so it reads
|
|
23
|
+
UNKNOWN today, and both the emit and the ledger header now say so plainly.
|
|
24
|
+
- **A torn spine line stops reading as exact spend.** `scripts/run-battery-readshim.mjs` marked
|
|
25
|
+
unparseable lines `__corrupt` "never guessed at", then counted them, dropped them, and reported
|
|
26
|
+
the row's spend as EXACT while `corruptLines` reached no readout. `spendComplete` now goes false
|
|
27
|
+
when any line was unreadable (spend renders `>=`) and the audit prints the count per row.
|
|
28
|
+
- **G1's ceiling half is refused at $0 instead of after the drafter is paid.** `validatePlan` reds
|
|
29
|
+
`read-blind` when a capped step grants `read` without the retrieval pair — but a step cannot
|
|
30
|
+
grant what the SIGNED CEILING does not offer, so against a spec whose `tools` lack `recall`/`get`
|
|
31
|
+
**every** draft red identically and the drafter was paid for each doomed cycle. `runPlan` now
|
|
32
|
+
throws at the same $0 door as the arm guard, in the same class: an operator param-guard
|
|
33
|
+
`TypeError`, never a model-output red — nothing the agent authored is wrong when the operator
|
|
34
|
+
asks for a shim the signature cannot satisfy. Tighten-only; the only configuration it rejects
|
|
35
|
+
already failed 100% of the time, just later and for money. `RETRIEVAL_PAIR` is now exported from
|
|
36
|
+
`src/plan.js` rather than respelled, so the rule and its pre-flight cannot drift apart.
|
|
37
|
+
|
|
38
|
+
### Changed (2026-08-23)
|
|
39
|
+
|
|
40
|
+
- **The aurora close moved to a tree development cannot mutate** (`jobs/aurora-u-spawner-types.json`,
|
|
41
|
+
F110). Its stages had pointed at a scratch worktree, which was removed during ordinary branch
|
|
42
|
+
work — every stage silently became `node: cannot find module`. The home before that was a shared
|
|
43
|
+
working checkout, where a close landing mid-edit dies as an instrument fault rather than a
|
|
44
|
+
verdict. Neither is a home: a checkout is *supposed* to go dirty and a scratch worktree is
|
|
45
|
+
*supposed* to be removable. **A close is the arbiter's instrument, so its host must have the
|
|
46
|
+
arbiter's lifetime.** Now a worktree pinned at a release tag, never edited, with its own
|
|
47
|
+
dependencies. Re-signing was required and given in-turn.
|
|
48
|
+
|
|
49
|
+
### Added (2026-08-18 — the read shim, flag-gated OFF)
|
|
50
|
+
|
|
51
|
+
- **`src/readshim.js` — a per-worker delivery ledger over the read tool**, off unless the new
|
|
52
|
+
`readShim` flag is set on `runJob`/`runPlan`. A read over 24KB (`READ_SHIM_CAP`) delivers its
|
|
53
|
+
next unseen slice plus a trusted steer at `ctx_recall`/`ctx_get`; a re-read of a file the
|
|
54
|
+
worker holds WHOLE and unchanged returns a short pointer instead of the bytes; a re-read of
|
|
55
|
+
a file it holds only PART of returns the next slice, never a pointer; any content change
|
|
56
|
+
re-delivers from the start. Measured on a $0 replay of 1,844 real `shell_read` actions from
|
|
57
|
+
143 archived gate audits: 48% of reads re-read unchanged bytes, and cap+pointer keyed on what
|
|
58
|
+
was DELIVERED saved 65.6% of read payload with **zero** untruthful responses — where the
|
|
59
|
+
naive path+hash key saved 8.2 points more by telling **250 lies**, median 73,348 bytes hidden
|
|
60
|
+
each (BA-17 read-blinding, reproduced).
|
|
61
|
+
- **G1 at the validation gate (`read-blind`)**: under the shim, a plan step granting `read` must
|
|
62
|
+
also grant `recall` and `get`. Capping a worker with no retrieval verb is BA-17 on purpose —
|
|
63
|
+
the cap is what makes the pair mandatory. Stated in the drafting prompt as well as enforced
|
|
64
|
+
(the mailbox precedent).
|
|
65
|
+
- **One flag, four ARMS, default OFF** (the `layerRoot` precedent, F41). `readShim` names which
|
|
66
|
+
levers run, one value per row of the frozen Phase 2 pre-registration: `false` = A0 (off),
|
|
67
|
+
`'cap'` = A1 (cap + pointer + next-unseen-slice + G1), `'diff'` = A2 (the diff lever alone — no
|
|
68
|
+
cap, no pointer, no G1), `true` = A3 (every lever). The two booleans mean exactly what they
|
|
69
|
+
meant before the arms existed, so nothing written against them changed meaning. With the shim
|
|
70
|
+
off every observable — validator reds, persona, delivered bytes — is byte-identical to the
|
|
71
|
+
pre-shim run: the frozen A0 arm has to be exactly today, and a guard firing under a disabled
|
|
72
|
+
shim would make the baseline a treatment arm. Each arm's persona line describes only the
|
|
73
|
+
machinery actually installed (A2 is never told about a 24KB limit that is not in force). An
|
|
74
|
+
unrecognised value THROWS at the entry (`readShimArm`) rather than coercing into a truthy
|
|
75
|
+
shim — a mis-spelled arm would run one treatment under another's label and be invisible in the
|
|
76
|
+
results afterwards. The default-flip is not in this change: it waits on a paid contrast.
|
|
77
|
+
- **A stale `ctx_get` pointer serves the requested line range from disk** (2026-08-19), under a
|
|
78
|
+
CAPPING arm only. The cap's own strategy line steers a capped worker at `ctx_recall` →
|
|
79
|
+
`ctx_get` for a whole function; `ctx_get` is content-hash gated and throws `StalePointerError`
|
|
80
|
+
the moment the file changed after indexing — which is the normal case for a worker that just
|
|
81
|
+
EDITED the file it is working on. Measured on the A1 arm's three green runs: 10 `ctx_get`
|
|
82
|
+
calls, 5 stale, 0 bytes each, every one a just-written file. A paid round returning nothing,
|
|
83
|
+
down a path the cap itself chose. The serve hands back the requested 0-based inclusive line
|
|
84
|
+
range read fresh from disk, under TRUSTED framing that says the pointer was stale, that the
|
|
85
|
+
range may now hold DIFFERENT code than the symbol asked for, which lines arrived, and that
|
|
86
|
+
nothing was recorded as delivered. litectx refuses to slice a drifted range itself because an
|
|
87
|
+
UNLABELLED slice can silently be another symbol's body — the label is what makes it legal.
|
|
88
|
+
Detection is on the exported `StalePointerError` CLASS, never on message prose. A vanished
|
|
89
|
+
file or a range now entirely past the end is a refusal RESULT naming what happened, never a
|
|
90
|
+
throw. It **never touches the delivery ledger**: a slice is not the file, and a ledger that
|
|
91
|
+
recorded one would answer the next read with "you already have it" to a worker holding 90
|
|
92
|
+
lines of 900 — the untruthful-pointer failure the shim exists to prevent. The cost of that is
|
|
93
|
+
a later `shell_read` may re-deliver bytes the serve already showed; the shim over-delivers
|
|
94
|
+
rather than lies. A0 and A2 are untouched: with no capping arm the hook is not wired and
|
|
95
|
+
`ctx_get`'s contract is byte-identical.
|
|
96
|
+
- On the native surface the shim REPLACES the `NATIVE_READ_CAP` wrapper rather than layering over
|
|
97
|
+
it (both bound the same seam at the same 24KB, and a shim outside the native wrapper would
|
|
98
|
+
ledger already-truncated text as a whole file — the exact lie it exists to prevent). L2
|
|
99
|
+
(diff-on-change) is deliberately not built: it never fired on the real corpus.
|
|
100
|
+
|
|
8
101
|
## [0.12.0] — 2026-08-23
|
|
9
102
|
|
|
10
103
|
### Added (N4 slice 2 — the softgreen rung: a close that JUDGES, and a signer who has the last word)
|
package/bareloop.context.md
CHANGED
|
@@ -918,7 +918,7 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
|
|
|
918
918
|
`coordination-red` — a failure between units (scope contention, step order, store
|
|
919
919
|
races), never to be folded into worker/interpreter reds.
|
|
920
920
|
|
|
921
|
-
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun? })` → outcome — `src/run.js`
|
|
921
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, readShim?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun? })` → outcome — `src/run.js`
|
|
922
922
|
|
|
923
923
|
The last seven are the RESUME fold and are documented under *Resuming a killed run* below; they
|
|
924
924
|
default to `0` / `true` / `0` / `null` / `[]` / `null` / `null`, so a fresh run passes none of them.
|
|
@@ -937,6 +937,56 @@ plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red
|
|
|
937
937
|
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | hitl-pause |
|
|
938
938
|
hitl-decision-red | step-red:<id>`.
|
|
939
939
|
|
|
940
|
+
**`readShim` (default `false`) — the capped read seam, per ARM.** The value names which levers
|
|
941
|
+
run — the four arms of the frozen Phase 2 pre-registration:
|
|
942
|
+
|
|
943
|
+
| value | arm | cap + next-unseen-slice | pointer | diff on a changed re-read | G1 (`read-blind`) | persona line |
|
|
944
|
+
|---|---|---|---|---|---|---|
|
|
945
|
+
| `false` *(default)* | A0 | — | — | — | — | none |
|
|
946
|
+
| `'cap'` | A1 | ✓ | ✓ | — | ✓ | `READ_SHIM_STRATEGY` |
|
|
947
|
+
| `'diff'` | A2 | — | — | ✓ | — | `READ_SHIM_DIFF_STRATEGY` |
|
|
948
|
+
| `true` | A3 | ✓ | ✓ | ✓ | ✓ | `READ_SHIM_STRATEGY` |
|
|
949
|
+
|
|
950
|
+
Anything else **throws** (`readShimArm`) at `runJob`/`runPlan`/`validatePlan` before a token is
|
|
951
|
+
spent — a mis-spelled arm coerced by truthiness would run one treatment under another's label and
|
|
952
|
+
be invisible in the results afterwards.
|
|
953
|
+
|
|
954
|
+
A **capping arm against a signed ceiling that offers no `recall`/`get` also throws at that same
|
|
955
|
+
$0 door**, and for the same reason: G1 below requires the retrieval pair on any step granting
|
|
956
|
+
`read`, but a step cannot grant what `spec.tools` does not offer — so every draft would red
|
|
957
|
+
`read-blind` identically while the drafter was paid for each doomed cycle. The message names the
|
|
958
|
+
missing verbs and both exits (re-sign the spec with them, or run with the shim off). Like the arm
|
|
959
|
+
throw, it is an **operator param-guard `TypeError`, never a model-output red**: nothing the agent
|
|
960
|
+
authored is wrong when the operator asks for a shim the signature cannot satisfy. It narrows
|
|
961
|
+
nothing that works — the only configuration it rejects already failed 100% of the time, later and
|
|
962
|
+
for money. `RETRIEVAL_PAIR` is exported from `src/plan.js` so the rule and this pre-flight read one
|
|
963
|
+
spelling of which verbs count.
|
|
964
|
+
|
|
965
|
+
Under a CAPPING arm (A1/A3) the shim also answers a **stale `ctx_get` pointer**: the cap steers the
|
|
966
|
+
worker at `ctx_recall`/`ctx_get`, and `ctx_get` refuses a file that changed after indexing — the
|
|
967
|
+
normal case once the worker has edited it. Instead of nothing, the shim serves that pointer's
|
|
968
|
+
0-based inclusive line range read fresh from disk, capped the same way, under a trusted notice
|
|
969
|
+
saying the pointer was stale, that the range may now hold DIFFERENT code than the symbol asked
|
|
970
|
+
for, and that nothing was recorded as delivered. A vanished file or a range past the end is a
|
|
971
|
+
refusal result, never a throw. The serve never writes the delivery ledger, so it can never mint a
|
|
972
|
+
"you already hold it" pointer for bytes the read seam did not hand over. A0 and A2 do not wire it
|
|
973
|
+
and `ctx_get` behaves exactly as it always did.
|
|
974
|
+
|
|
975
|
+
`false` is the shipped behaviour and is byte-identical to a run without the flag, guards included;
|
|
976
|
+
`true` means exactly what it meant before the arms existed, so nothing written against the boolean
|
|
977
|
+
changed meaning. Where the CAP runs, `shell_read` delivers at most `READ_SHIM_CAP` (24KB) per call
|
|
978
|
+
with a trusted steer at `ctx_recall`/`ctx_get`; a re-read continues from where the last one
|
|
979
|
+
stopped, and once the worker holds the whole unchanged file a re-read returns a pointer instead of
|
|
980
|
+
the bytes (keyed on what was DELIVERED — a partly-seen file NEVER gets a pointer, which is the
|
|
981
|
+
measured correctness point); and `validatePlan` reds `read-blind` on any step granting `read`
|
|
982
|
+
without `recall` and `get`. Where only the DIFF runs, nothing is capped (a first read delivers
|
|
983
|
+
whole, whatever the size), an unchanged re-read re-delivers rather than pointing, and G1 does not
|
|
984
|
+
exist — G1 is the cap's compensation, and an arm that caps nothing has nothing to compensate for.
|
|
985
|
+
The ledger is per worker, so it resets with every step. On the native (clipipe) surface a CAPPING
|
|
986
|
+
arm replaces the CLI-display cap rather than stacking on it; a non-capping arm leaves that wrapper
|
|
987
|
+
installed *inside* the shim, so native never loses its bound. Default-flip pending a paid contrast;
|
|
988
|
+
see CHANGELOG for the replay numbers behind it.
|
|
989
|
+
|
|
940
990
|
**The two hitl terminals (N4 slice 1, doors re-cut 2026-08-18)** are the class's whole surface
|
|
941
991
|
at this layer, and each is a CLEAN exit (`spendComplete` stays true — only the two casualties floor). `hitl-pause` is a
|
|
942
992
|
decision-ready CHECKPOINT: the close reached a stage no machine can render, so the run stops
|
|
@@ -1393,6 +1443,53 @@ reserved round bound, with the LAST round reserved for a toolless summary: the b
|
|
|
1393
1443
|
a scout mid-tool-use, and text is its only deliverable, so without that reservation it
|
|
1394
1444
|
spends its whole allowance exploring and reports nothing (F59 — measured on 15 of 18 runs).
|
|
1395
1445
|
|
|
1446
|
+
**Every cost figure bareloop reports is an ESTIMATE, not a billed amount (BA-21).** No
|
|
1447
|
+
vanilla LLM API returns a price — providers report TOKENS — so `spentUsd`, every
|
|
1448
|
+
`worker-round` cost, every halt readout and every ledger dollar is a token count
|
|
1449
|
+
multiplied by a RATE, never a provider invoice. Expect it to read somewhat high, and do
|
|
1450
|
+
not reconcile it against a bill expecting a match. There is deliberately **no per-model
|
|
1451
|
+
price table** behind it, and the reason is worth knowing before you decide how far to
|
|
1452
|
+
trust the number: a hand-curated table is a treadmill that rots SILENTLY — a price moves
|
|
1453
|
+
or a new model ships, the row goes stale, and the figure under-reports with no detector to
|
|
1454
|
+
say so. bare-agent instead matches a recognized Claude TIER (haiku / sonnet) by model id,
|
|
1455
|
+
and falls back to a conservative ceiling rate for anything it does not recognize. Both
|
|
1456
|
+
deliberately OVER-report rather than under-report, which is the fail-safe direction for a
|
|
1457
|
+
cap: a run halts slightly early rather than overspending unnoticed. **The consequence
|
|
1458
|
+
bites at the budget, and it is not softened here:** `budgetUsd` and `shellCapUsd` are
|
|
1459
|
+
enforced against these estimates, so an inaccurate rate is a cap that binds earlier or
|
|
1460
|
+
later than you intended. The cap is exact about the number it holds; the number is a
|
|
1461
|
+
guess.
|
|
1462
|
+
|
|
1463
|
+
**If you need accuracy, pass your own rates — the supported path, not a workaround.**
|
|
1464
|
+
bare-agent's `Loop` takes `rates: {in, out, cacheReadMult?, cacheWriteMult?}` — USD per 1K
|
|
1465
|
+
tokens, the two multipliers applying to the input rate for the cache-read and cache-write
|
|
1466
|
+
tiers (defaulting to Anthropic's 0.1× / 1.25×). A caller-supplied rate is recorded as
|
|
1467
|
+
VOUCHED rather than as a guess, and it silences bare-agent's own guesstimate warning.
|
|
1468
|
+
**Named, not papered over:** `runJob`/`runPlan` construct their own `Loop` and do not yet
|
|
1469
|
+
accept a `rates` passthrough, so today that option is reachable only by a caller driving
|
|
1470
|
+
bare-agent directly — a bareloop-run job is priced by the guesstimate, full stop. The
|
|
1471
|
+
passthrough is an open follow-up, and until it lands every paragraph above describes the
|
|
1472
|
+
only pricing a `runJob` adopter gets.
|
|
1473
|
+
|
|
1474
|
+
**The provenance is on the record, per round (`rateSource`).** The field arrives with
|
|
1475
|
+
bare-agent **>= 0.37**; under the pinned `^0.36.0` no provider payload carries it yet, so
|
|
1476
|
+
every priced round this library writes today reads UNKNOWN provenance — correctly, and by
|
|
1477
|
+
the same rule that governs the archive. (The one exception is the native per-turn
|
|
1478
|
+
`worker-turn`, whose `null` is bareloop's OWN statement rather than a forwarded one: that
|
|
1479
|
+
surface prices the SESSION, so a turn had no rate to guess.) Once the pin moves, every `worker-round` /
|
|
1480
|
+
`worker-turn` the plan flow writes carries bare-agent's own label beside `pricing`:
|
|
1481
|
+
`'provider'` (the provider reported its own authoritative cost — the native CLI surface)
|
|
1482
|
+
and `'caller'` (you passed the rate) are VOUCHED; `'tier'` (a recognized Claude tier,
|
|
1483
|
+
where **both** of bareloop's own production models land) and `'default'` (the blind
|
|
1484
|
+
ceiling) are GUESSES; `null` means nothing was priced, so there was no rate to have
|
|
1485
|
+
guessed. An ABSENT field is UNKNOWN provenance — every round archived before this signal
|
|
1486
|
+
existed, and it is never backfilled, never reconstructed, and never rounded up to vouched.
|
|
1487
|
+
Read it with `rateProvenance(record)` and `spendProvenance(events)` (`src/ledger.js`),
|
|
1488
|
+
whose predicate is an ALLOW-LIST of the two vouched values: anything else, including a
|
|
1489
|
+
value a future bare-agent adds, reads as a GUESS. **Reporting only** — `pricing` stays
|
|
1490
|
+
strictly two-valued (`'priced'`|`'unpriced'`), the `pricing-red` halt still keys on cost
|
|
1491
|
+
alone, and nothing about provenance decides, halts or refuses anything.
|
|
1492
|
+
|
|
1396
1493
|
**What the worker is told (tool mode):** the absolute repository root (F10 — bare-agent's
|
|
1397
1494
|
shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
|
|
1398
1495
|
blind), the close's CURRENT output as the tree's state (F13 — never framed as "your previous
|
|
@@ -1977,7 +2074,14 @@ the same refusal emits, so counting the escalation too would double every refusa
|
|
|
1977
2074
|
`suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
|
|
1978
2075
|
status rows (`open → filed → fixed → consumed`) are human-appended, and the fold shows
|
|
1979
2076
|
the latest per key. Pure pieces exported for custom folds: `classifyIncidents(events,
|
|
1980
|
-
{spine?})`, `foldLedger(rows)`, `ledgerDeltas(fold, occurrences)`.
|
|
2077
|
+
{spine?})`, `foldLedger(rows)`, `ledgerDeltas(fold, occurrences)`. Riding with them,
|
|
2078
|
+
and deliberately NOT a ledger class: `rateProvenance(record)` → `vouched|guessed|unpriced|unknown`
|
|
2079
|
+
and `spendProvenance(events)` → per-provenance `{rounds, usd, unpricedRounds}` buckets answer
|
|
2080
|
+
"how much of this run's spend was priced by a rate nobody vouched for" (BA-21 — see *Every cost
|
|
2081
|
+
figure bareloop reports is an ESTIMATE* above). A bucket's `usd` sums FINITE costs only and is a
|
|
2082
|
+
FLOOR whenever its `unpricedRounds` is non-zero (F6 — an unknown cost is counted, never summed as
|
|
2083
|
+
$0). `VOUCHED_RATE_SOURCES` is the frozen allow-list they key on. **Reporting only**: no incident,
|
|
2084
|
+
no class, no halt. CLI lands at N5; the
|
|
1981
2085
|
panel reads the same file at N6.
|
|
1982
2086
|
|
|
1983
2087
|
## Architecture
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
package/src/index.js
CHANGED
|
@@ -82,7 +82,12 @@ export { renderListing, selectionPrompt } from './selection.js';
|
|
|
82
82
|
// credit it has no way to release. STORAGE ONLY — nothing here selects, promotes or reuses
|
|
83
83
|
// a bridge; those stay parked on `layer-3-reuse`.
|
|
84
84
|
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow } from './reuse.js';
|
|
85
|
-
|
|
85
|
+
// BA-21 pricing provenance rides out with the ledger because it is the same job: reading
|
|
86
|
+
// a run's own spend record honestly. `rateProvenance`/`spendProvenance` answer "how much of
|
|
87
|
+
// this was priced by a rate nobody vouched for" — REPORTING ONLY, no halt, no refusal.
|
|
88
|
+
// Exported because an adopter enforcing a budget against these numbers is entitled to know
|
|
89
|
+
// which of them are guesses (see bareloop.context.md, "Every cost figure … is an ESTIMATE").
|
|
90
|
+
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES, VOUCHED_RATE_SOURCES, rateProvenance, spendProvenance } from './ledger.js';
|
|
86
91
|
// THE WORK BRANCH (PRD v1.57 §3). `workBranchName` is exported so an operator runner can
|
|
87
92
|
// SHOW, before the approval gate, which branch the run will work on — the same reason the
|
|
88
93
|
// resolved per-try hash is printed there: a bound the human is agreeing to should not be
|
|
@@ -91,6 +96,16 @@ export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASS
|
|
|
91
96
|
// still there before spending anything. The runner applies the rule itself either way —
|
|
92
97
|
// neither export is a way to opt out of it.
|
|
93
98
|
export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from './workbranch.js';
|
|
99
|
+
// THE READ SHIM (arm-gated, OFF by default via `runJob`'s `readShim`). The cap is exported
|
|
100
|
+
// because bareloop.context.md names it, and documented-but-unexported is a false contract;
|
|
101
|
+
// the wrapper goes with it so a caller building its own worker seam wraps the SAME ledger
|
|
102
|
+
// rather than reimplementing the one rule that matters (a pointer keys on what was
|
|
103
|
+
// DELIVERED, never on path+hash).
|
|
104
|
+
//
|
|
105
|
+
// `readShimArm` is exported for the same reason the cap is: a battery driver has to be able
|
|
106
|
+
// to name and check the arm it is about to run BEFORE it launches, and the legal set living
|
|
107
|
+
// only inside a runner would leave the driver guessing at spellings the guard will reject.
|
|
108
|
+
export { createReadShim, wrapReadTool, readShimArm, readShimStrategy, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY, READ_SHIM_ARMS } from './readshim.js';
|
|
94
109
|
// ── CLOSE AUTHORING v1 (gate 4) — the user declares what done means ──────────
|
|
95
110
|
// The public surface is settled ONCE, here, at M4 (M2's header flagged the
|
|
96
111
|
// naming collision and deferred it rather than exporting piecemeal).
|
package/src/ledger.js
CHANGED
|
@@ -404,3 +404,105 @@ export function updateLedger({ ledgerFile, spineFiles }) {
|
|
|
404
404
|
});
|
|
405
405
|
return { appended, fold: foldLedger(rows.concat(appended)) };
|
|
406
406
|
}
|
|
407
|
+
|
|
408
|
+
// ── BA-21 pricing provenance: the READ side (REPORTING ONLY) ─────────────────
|
|
409
|
+
// bare-agent >=0.37 rides `rateSource` beside `pricing` on every metering payload,
|
|
410
|
+
// and the plan flow forwards it verbatim onto `worker-round`/`judge-round`/`worker-turn`
|
|
411
|
+
// (`rateSourceFields`, src/planrun.js — the write side).
|
|
412
|
+
//
|
|
413
|
+
// NAMED rather than papered over: `judge-round`'s own payload (src/kinds.js's
|
|
414
|
+
// `onJudgeCost({...})`) does not carry `rateSource` yet, so every judge round reads
|
|
415
|
+
// UNKNOWN provenance today — correctly, by the same rule that governs the archive, and
|
|
416
|
+
// the day that payload forwards the field the emit site already spreads it.
|
|
417
|
+
//
|
|
418
|
+
// `pricing` is UNCHANGED and still strictly two-valued ('priced'|'unpriced'). Nothing
|
|
419
|
+
// below reads it, nothing below decides anything, and no new escalation category exists:
|
|
420
|
+
// the `pricing-red` halt in src/run.js keys on cost alone and stays the ONLY pricing
|
|
421
|
+
// signal with teeth. This is a READOUT — how much of a run's spend was priced by a rate
|
|
422
|
+
// nobody vouched for — and coupling it to run control would be a new refusal, which is
|
|
423
|
+
// arbiter territory.
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The two rate sources somebody VOUCHED for: `'provider'` (the provider reported its own
|
|
427
|
+
* authoritative cost, e.g. the claude CLI's `total_cost_usd`) and `'caller'` (we passed
|
|
428
|
+
* the rate in ourselves). Everything else bare-agent can report is a built-in guess.
|
|
429
|
+
*
|
|
430
|
+
* An ALLOW-LIST of vouched values, never a deny-list of guessed ones — this is the
|
|
431
|
+
* load-bearing line of the whole signal, for two reasons.
|
|
432
|
+
*
|
|
433
|
+
* First: BOTH of bareloop's production models resolve to `'tier'`, not to `'default'`
|
|
434
|
+
* (`claude-sonnet-5` and `claude-haiku-4-5-…` each match a recognized Claude tier by
|
|
435
|
+
* model-id substring, so bare-agent prices them off a hardcoded tier rate that could
|
|
436
|
+
* have drifted from Anthropic's). A "is this a guess?" test spelled
|
|
437
|
+
* `rateSource === 'default'` would therefore read EVERY normal bareloop round as
|
|
438
|
+
* confidently priced and see none of the population it exists to measure — this repo's
|
|
439
|
+
* blind-instrument class, seven recorded instances, and not becoming the eighth.
|
|
440
|
+
*
|
|
441
|
+
* Second: written this way, any value bare-agent adds later that we do not recognise
|
|
442
|
+
* reads as a GUESS rather than as vouched. The fail-safe direction, by construction.
|
|
443
|
+
*/
|
|
444
|
+
export const VOUCHED_RATE_SOURCES = Object.freeze(['provider', 'caller']);
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Read one spend record's rate PROVENANCE. Four-way, and the two absent-ish cases are
|
|
448
|
+
* deliberately distinct facts:
|
|
449
|
+
* - `'vouched'` — a `VOUCHED_RATE_SOURCES` value: somebody stands behind the rate.
|
|
450
|
+
* - `'guessed'` — a rate was used and nobody vouched for it (`'tier'`, `'default'`, or
|
|
451
|
+
* any future value we do not recognise).
|
|
452
|
+
* - `'unpriced'` — `rateSource: null`: nothing was priced, so there was no rate to have
|
|
453
|
+
* guessed. Upstream said so; we are not inferring it.
|
|
454
|
+
* - `'unknown'` — the record carries no `rateSource` at all. Every round archived before
|
|
455
|
+
* this signal existed lands here and is NEVER backfilled: an absent label
|
|
456
|
+
* is reported as unknown, never reconstructed, and never rounded up to
|
|
457
|
+
* vouched (F6's unknown-is-never-zero, applied to provenance).
|
|
458
|
+
* @param {any} record a `worker-round` / `judge-round` / `worker-turn` spine record
|
|
459
|
+
* @returns {'vouched'|'guessed'|'unpriced'|'unknown'}
|
|
460
|
+
*/
|
|
461
|
+
export function rateProvenance(record) {
|
|
462
|
+
if (!record || typeof record !== 'object' || !('rateSource' in record)) return 'unknown';
|
|
463
|
+
const source = /** @type {any} */ (record).rateSource;
|
|
464
|
+
if (source === undefined) return 'unknown'; // a key carrying no value taught us nothing
|
|
465
|
+
if (source === null) return 'unpriced';
|
|
466
|
+
return VOUCHED_RATE_SOURCES.includes(source) ? 'vouched' : 'guessed';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The spend records a run's money is read from. `worker-round` and `judge-round` are the
|
|
470
|
+
* ACCOUNTED types — exactly `ACCOUNTED_ROUND_TYPES` in src/run.js, which is the ledger that
|
|
471
|
+
* enforces the budget (F12). `judge-round` is the softgreen rung's judged floor: the first
|
|
472
|
+
* spend a CLOSE has ever had, and a provenance readout that skipped it would be blind to the
|
|
473
|
+
* one seam whose rate nobody has audited. `worker-turn` is the native surface's per-turn
|
|
474
|
+
* attribution — unpriced by design, included so the readout covers every metering row the
|
|
475
|
+
* plan flow writes rather than only the ones carrying money. The attempt-level totals
|
|
476
|
+
* (`worker-result`, `worker-plan`) are ECHOES of these same rounds and are excluded, for
|
|
477
|
+
* the same reason the ledger in run.js excludes them: counting them double-counts.
|
|
478
|
+
*
|
|
479
|
+
* Re-spelled rather than imported (src/run.js imports this module's siblings and a cycle
|
|
480
|
+
* buys nothing here) — `tests/ledger.test.js` asserts the two spellings agree, because a
|
|
481
|
+
* spend-slicing instrument that misses a writer is the F45 class this list exists to avoid. */
|
|
482
|
+
const SPEND_RECORD_TYPES = Object.freeze(['worker-round', 'judge-round', 'worker-turn']);
|
|
483
|
+
|
|
484
|
+
/** @typedef {{rounds: number, usd: number, unpricedRounds: number}} ProvenanceBucket */
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* How much of a run's spend was priced by a guess. Pure fold over one spine's events,
|
|
488
|
+
* REPORTING ONLY — it mints nothing, halts nothing and refuses nothing.
|
|
489
|
+
*
|
|
490
|
+
* `usd` sums only FINITE costs; a round whose cost is not a finite number is counted in
|
|
491
|
+
* `unpricedRounds` instead of being summed as $0 (F6 — unpriced is never free), so a
|
|
492
|
+
* bucket's `usd` is a FLOOR whenever its `unpricedRounds` is non-zero.
|
|
493
|
+
* @param {any[]} events one spine's parsed events
|
|
494
|
+
* @returns {{vouched: ProvenanceBucket, guessed: ProvenanceBucket, unpriced: ProvenanceBucket, unknown: ProvenanceBucket}}
|
|
495
|
+
*/
|
|
496
|
+
export function spendProvenance(events) {
|
|
497
|
+
/** @returns {ProvenanceBucket} */
|
|
498
|
+
const bucket = () => ({ rounds: 0, usd: 0, unpricedRounds: 0 });
|
|
499
|
+
const out = { vouched: bucket(), guessed: bucket(), unpriced: bucket(), unknown: bucket() };
|
|
500
|
+
for (const ev of events ?? []) {
|
|
501
|
+
if (!ev || !SPEND_RECORD_TYPES.includes(ev.type)) continue;
|
|
502
|
+
const b = out[rateProvenance(ev)];
|
|
503
|
+
b.rounds += 1;
|
|
504
|
+
if (typeof ev.costUsd === 'number' && Number.isFinite(ev.costUsd)) b.usd += ev.costUsd;
|
|
505
|
+
else b.unpricedRounds += 1;
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
package/src/plan.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { TOOL_MENU, LOCKED_TOOLS, WRITE_VERBS, checkMenu } from './job.js';
|
|
28
28
|
import { declaredStages } from './declaredclose.js';
|
|
29
29
|
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
|
|
30
|
+
import { READ_SHIM_CAP, readShimArm } from './readshim.js';
|
|
30
31
|
|
|
31
32
|
/** the closed exit menu (PRD v1.12 §3 + decision 1's `check-passes`): the
|
|
32
33
|
* shell evaluates every form with its own fixed code, never a command — the
|
|
@@ -49,6 +50,14 @@ export { WRITE_VERBS };
|
|
|
49
50
|
* count. Shallow entries survive the cap (see `legalScopes`) because they are
|
|
50
51
|
* the useful ones — a deep leaf is reachable via its parent. */
|
|
51
52
|
export const MAX_SCOPE_MENU = 24;
|
|
53
|
+
/** G1's pair (read shim only): the two verbs that can reach INTO a file the cap
|
|
54
|
+
* cut short — `recall` names where a symbol lives, `get` reads that symbol in
|
|
55
|
+
* full. Named here because the rule that needs them lives here; the cap that
|
|
56
|
+
* makes them mandatory lives in src/readshim.js.
|
|
57
|
+
* EXPORTED because `runPlan` refuses the same pair at $0 (see the G1 ceiling
|
|
58
|
+
* guard there): the rule and its pre-flight must read ONE spelling of which
|
|
59
|
+
* verbs count, never two that can drift apart. */
|
|
60
|
+
export const RETRIEVAL_PAIR = Object.freeze(['recall', 'get']);
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
63
|
* The MENU of legal `tree-changed` scopes — choose-don't-describe (design record
|
|
@@ -202,7 +211,7 @@ export function closeStagesOf(job) {
|
|
|
202
211
|
* job spec. Never throws on JSON text or plain parsed data; every failure is
|
|
203
212
|
* a named red. Returns the parsed plan on ok (single parse), null on any red.
|
|
204
213
|
* @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
|
|
205
|
-
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
|
|
214
|
+
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[], readShim?: boolean|'cap'|'diff' }} [opts] `job`: the
|
|
206
215
|
* validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
|
|
207
216
|
* menu all come from it — a missing or non-plan-shape job fails CLOSED);
|
|
208
217
|
* `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
|
|
@@ -224,9 +233,23 @@ export function closeStagesOf(job) {
|
|
|
224
233
|
* only). Rule B — a redraft may not drop one (`check-shed`): exits without
|
|
225
234
|
* the check verify FORM alone, so a shed lets the run "green" unearned
|
|
226
235
|
* (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
|
|
236
|
+
* `readShim` (G1): the read shim's ARM for this run — `false` (default), `'cap'`,
|
|
237
|
+
* `'diff'` or `true`. The arms that CAP (`'cap'`, `true`) add one rule: a step
|
|
238
|
+
* granting `read` must also grant `recall` and `get` (`read-blind`), because a
|
|
239
|
+
* capped read is only navigable with them. The arms that do not cap (`false`,
|
|
240
|
+
* `'diff'`) render byte-identically to the pre-shim validator, so the frozen A0
|
|
241
|
+
* baseline stays a baseline and A2 isolates the diff instead of importing an
|
|
242
|
+
* admission rule with it. An unrecognised value THROWS (`readShimArm`) — a param
|
|
243
|
+
* guard on the caller's own argument, never a plan red.
|
|
227
244
|
* @returns {{ ok: boolean, reds: Red[], plan: object|null }}
|
|
228
245
|
*/
|
|
229
|
-
export function validatePlan(input, { job, maxStepRounds = 40, scopes, seedRed, priorChecks } = {}) {
|
|
246
|
+
export function validatePlan(input, { job, maxStepRounds = 40, scopes, seedRed, priorChecks, readShim = false } = {}) {
|
|
247
|
+
// The one thing in here that throws, and it is not plan data: an unknown arm
|
|
248
|
+
// name is the CALLER's own argument, checked before a single plan field is
|
|
249
|
+
// read. The "never throws" contract above is about the agent's artifact —
|
|
250
|
+
// this is the operator's, and silently coercing it would run one arm under
|
|
251
|
+
// another's label (the BA-4 param-guard class; see `readShimArm`).
|
|
252
|
+
const shimArm = readShimArm(readShim);
|
|
230
253
|
/** @type {Red[]} */
|
|
231
254
|
const reds = [];
|
|
232
255
|
/** @type {(code: string, path: string, detail?: string) => void} */
|
|
@@ -335,6 +358,24 @@ export function validatePlan(input, { job, maxStepRounds = 40, scopes, seedRed,
|
|
|
335
358
|
}
|
|
336
359
|
writeStep = s.tools.some((/** @type {string} */ t) => WRITE_VERBS.includes(t));
|
|
337
360
|
toolsParsed = true;
|
|
361
|
+
// G1 — the read shim's admission rule, and it only exists while the CAP
|
|
362
|
+
// does. The shim caps a read at READ_SHIM_CAP and hands back a steer;
|
|
363
|
+
// capping a worker whose grant holds no retrieval verb leaves it with no
|
|
364
|
+
// way to reach the rest of the file, which is BA-17 read-blinding on
|
|
365
|
+
// purpose. So the cap is what makes the pair mandatory — the two ship
|
|
366
|
+
// together or neither ships.
|
|
367
|
+
// Fires only inside the parsed branch (the mailbox precedent: a rule
|
|
368
|
+
// derived from an unknowable grant charges the ledger for a violation
|
|
369
|
+
// the agent never committed), and only per step, because the grant IS
|
|
370
|
+
// the step boundary. Keyed on the ARM's g1 flag, not on "is the shim on":
|
|
371
|
+
// the diff-only arm (A2) caps nothing, so there is nothing to be blind
|
|
372
|
+
// about, and firing here would narrow A2's admissible plan space on top
|
|
373
|
+
// of the lever it is meant to isolate.
|
|
374
|
+
if (shimArm.g1 && s.tools.includes('read') && !RETRIEVAL_PAIR.every((/** @type {string} */ v) => s.tools.includes(v))) {
|
|
375
|
+
const short = RETRIEVAL_PAIR.filter((v) => !ceiling.includes(v));
|
|
376
|
+
red('read-blind', `${at}.tools`, `a step granting "read" must also grant ${RETRIEVAL_PAIR.join(' and ')} — reads are capped at ${READ_SHIM_CAP} bytes and the rest of a big file is reachable only through the retrieval verbs; a capped worker without them is blind to it`
|
|
377
|
+
+ (short.length ? ` (the signed ceiling [${ceiling.join(', ')}] offers no ${short.join('/')}: this plan cannot satisfy the rule — the spec needs re-signing with them, or the run needs the shim off)` : ''));
|
|
378
|
+
}
|
|
338
379
|
}
|
|
339
380
|
|
|
340
381
|
// rounds ≤ the shell cap (cap-not-estimate; the step bound IS maxTurns)
|