bareloop 0.12.0 → 0.14.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 CHANGED
@@ -5,6 +5,145 @@ 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.14.0] — 2026-08-25
11
+
12
+ ### Added
13
+
14
+ - **One transport-class retry** (F115, hamr's ruling): the ONE worker-Loop seam in
15
+ `src/planrun.js` now retries exactly once on a transport-only throw (TLS fault,
16
+ `ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` over a network cause — never an HTTP
17
+ 4xx/5xx/429 response), classified by the new `src/transport.js` (`isTransportFailure`,
18
+ fixed `TRANSPORT_MAX_ATTEMPTS`, never operator-configurable). Emits a report-only
19
+ `transport-retry` spine record and forces `job-end.spendComplete: false` for the rest of the
20
+ run — the retried attempt's first throw may already have been billed — surfaced by
21
+ `scripts/run-u.mjs` as a `retries` line only when a retry occurred.
22
+ - **provider-red joins the resumable set** (F115, PRD v1.80 TODO #4): when the one transport
23
+ retry also fails and a `run-u` run ends `provider-red`, its tail now prints an honest
24
+ readout — spend so far as a floor, "died in step X of Y" (or "before a plan was accepted"),
25
+ and the exact `--resume` command — and `--resume` re-enters at the recorded step with the
26
+ accepted plan, folding prior spend in. No cost/step threshold gates the offer (the operator
27
+ decides, every time); verdict terminals (`escalated`/`step-red`/`plan-red`) still refuse.
28
+ Folded from a local `scripts/run-u.mjs` widening into the library's own canonical
29
+ `CHECKPOINT_OUTCOMES` (`src/reuse.js`) so the two lists cannot drift.
30
+ - **Run behaviour summary** (build-list item 2, agreed 2026-08-23): `runBehaviour(events, {
31
+ runId? })` and `formatBehaviour(summary)` in `src/behaviour.js`, plus
32
+ `scripts/behaviour-readout.mjs <gate-audit.jsonl> [run_id]`. A report-only text block —
33
+ "94 tool calls · 61 read, 28 grep, 5 recall" / "17 exact repeats (~18%)" — computed entirely
34
+ from a run's own gate-audit JSONL. Gates nothing, changes no verdict, writes nothing to the
35
+ spine. The exact-repeat key is the full recorded action (`type` + `path` + every `args` key),
36
+ never collapsed to `{type, path}` — sharper collapse would have merged different byte ranges
37
+ of the same file into false repeats.
38
+ - **`memory-cache` end-of-run readout** for the read shim: `runPlan` emits one spine record
39
+ (`{pointered, capped, bytesWithheld, approxTokens}`) summing what L1's pointer and L4's cap
40
+ withheld across the whole run, only when the shim is armed. `scripts/run-u.mjs` prints it as
41
+ `MEMORY-CACHE N re-reads answered from memory · M reads capped · X KB withheld (~Tk tokens
42
+ not re-sent)`; `scripts/behaviour-readout.mjs` prints the same line given the run's spine as
43
+ an optional third positional. No cost fields — report-only, not a spend record.
44
+
45
+ ### Fixed
46
+
47
+ - **`scripts/readshim-battery.mjs`'s spend reader compared a resumed leg's own money against
48
+ the wrong job-end figure** (F116): `readSpend`'s `ledgerUsd` now reads
49
+ `job-end.engagementSpentUsd` (this leg's spend) when a spine carries one, falling back to
50
+ `job-end.spentUsd` only for a cold run — `spentUsd` on a resumed job-end is a CHAIN total,
51
+ not this spine's own spend. `chainUsd`/`priorUsd` are exposed as new, honest fields and never
52
+ folded into `accountedUsd`/`ledgerUsd`/`spendUsd`.
53
+
54
+ ## [0.13.0] — 2026-08-23
55
+
56
+ ### Fixed (2026-08-23 — the release round: a pointer that lied, a close with no home, a rule that charged for its own refusal)
57
+
58
+ - **The pointer no longer lies about a file emptied since it was read.** `src/readshim.js`'s
59
+ pointer branch tested `start >= total` alone, so `0 >= 0` held with **no ledger entry at all** —
60
+ the first read of an empty file and a re-read of a file TRUNCATED TO EMPTY since delivery were
61
+ indistinguishable, and both answered "you already hold all 0 bytes". A worker would have gone on
62
+ believing in content that was gone. The guard now requires a ledger entry whose hash still
63
+ matches. This is precisely the untruthful-pointer class the module exists to prevent, found
64
+ inside the module built to prevent it, and reproduced live before the fix.
65
+ - **`judge-round` carries its rate provenance.** The judged floor made it the first spend a CLOSE
66
+ has ever had, and `src/ledger.js` documented the write side as forwarding `rateSource` onto it —
67
+ but the emit never spread the field, so every judge round would have read UNKNOWN forever. NOT
68
+ rounded up: `src/kinds.js`'s `onJudgeCost` payload still does not carry the field, so it reads
69
+ UNKNOWN today, and both the emit and the ledger header now say so plainly.
70
+ - **A torn spine line stops reading as exact spend.** `scripts/run-battery-readshim.mjs` marked
71
+ unparseable lines `__corrupt` "never guessed at", then counted them, dropped them, and reported
72
+ the row's spend as EXACT while `corruptLines` reached no readout. `spendComplete` now goes false
73
+ when any line was unreadable (spend renders `>=`) and the audit prints the count per row.
74
+ - **G1's ceiling half is refused at $0 instead of after the drafter is paid.** `validatePlan` reds
75
+ `read-blind` when a capped step grants `read` without the retrieval pair — but a step cannot
76
+ grant what the SIGNED CEILING does not offer, so against a spec whose `tools` lack `recall`/`get`
77
+ **every** draft red identically and the drafter was paid for each doomed cycle. `runPlan` now
78
+ throws at the same $0 door as the arm guard, in the same class: an operator param-guard
79
+ `TypeError`, never a model-output red — nothing the agent authored is wrong when the operator
80
+ asks for a shim the signature cannot satisfy. Tighten-only; the only configuration it rejects
81
+ already failed 100% of the time, just later and for money. `RETRIEVAL_PAIR` is now exported from
82
+ `src/plan.js` rather than respelled, so the rule and its pre-flight cannot drift apart.
83
+
84
+ ### Changed (2026-08-23)
85
+
86
+ - **The aurora close moved to a tree development cannot mutate** (`jobs/aurora-u-spawner-types.json`,
87
+ F110). Its stages had pointed at a scratch worktree, which was removed during ordinary branch
88
+ work — every stage silently became `node: cannot find module`. The home before that was a shared
89
+ working checkout, where a close landing mid-edit dies as an instrument fault rather than a
90
+ verdict. Neither is a home: a checkout is *supposed* to go dirty and a scratch worktree is
91
+ *supposed* to be removable. **A close is the arbiter's instrument, so its host must have the
92
+ arbiter's lifetime.** Now a worktree pinned at a release tag, never edited, with its own
93
+ dependencies. Re-signing was required and given in-turn.
94
+
95
+ ### Added (2026-08-18 — the read shim, flag-gated OFF)
96
+
97
+ - **`src/readshim.js` — a per-worker delivery ledger over the read tool**, off unless the new
98
+ `readShim` flag is set on `runJob`/`runPlan`. A read over 24KB (`READ_SHIM_CAP`) delivers its
99
+ next unseen slice plus a trusted steer at `ctx_recall`/`ctx_get`; a re-read of a file the
100
+ worker holds WHOLE and unchanged returns a short pointer instead of the bytes; a re-read of
101
+ a file it holds only PART of returns the next slice, never a pointer; any content change
102
+ re-delivers from the start. Measured on a $0 replay of 1,844 real `shell_read` actions from
103
+ 143 archived gate audits: 48% of reads re-read unchanged bytes, and cap+pointer keyed on what
104
+ was DELIVERED saved 65.6% of read payload with **zero** untruthful responses — where the
105
+ naive path+hash key saved 8.2 points more by telling **250 lies**, median 73,348 bytes hidden
106
+ each (BA-17 read-blinding, reproduced).
107
+ - **G1 at the validation gate (`read-blind`)**: under the shim, a plan step granting `read` must
108
+ also grant `recall` and `get`. Capping a worker with no retrieval verb is BA-17 on purpose —
109
+ the cap is what makes the pair mandatory. Stated in the drafting prompt as well as enforced
110
+ (the mailbox precedent).
111
+ - **One flag, four ARMS, default OFF** (the `layerRoot` precedent, F41). `readShim` names which
112
+ levers run, one value per row of the frozen Phase 2 pre-registration: `false` = A0 (off),
113
+ `'cap'` = A1 (cap + pointer + next-unseen-slice + G1), `'diff'` = A2 (the diff lever alone — no
114
+ cap, no pointer, no G1), `true` = A3 (every lever). The two booleans mean exactly what they
115
+ meant before the arms existed, so nothing written against them changed meaning. With the shim
116
+ off every observable — validator reds, persona, delivered bytes — is byte-identical to the
117
+ pre-shim run: the frozen A0 arm has to be exactly today, and a guard firing under a disabled
118
+ shim would make the baseline a treatment arm. Each arm's persona line describes only the
119
+ machinery actually installed (A2 is never told about a 24KB limit that is not in force). An
120
+ unrecognised value THROWS at the entry (`readShimArm`) rather than coercing into a truthy
121
+ shim — a mis-spelled arm would run one treatment under another's label and be invisible in the
122
+ results afterwards. The default-flip is not in this change: it waits on a paid contrast.
123
+ - **A stale `ctx_get` pointer serves the requested line range from disk** (2026-08-19), under a
124
+ CAPPING arm only. The cap's own strategy line steers a capped worker at `ctx_recall` →
125
+ `ctx_get` for a whole function; `ctx_get` is content-hash gated and throws `StalePointerError`
126
+ the moment the file changed after indexing — which is the normal case for a worker that just
127
+ EDITED the file it is working on. Measured on the A1 arm's three green runs: 10 `ctx_get`
128
+ calls, 5 stale, 0 bytes each, every one a just-written file. A paid round returning nothing,
129
+ down a path the cap itself chose. The serve hands back the requested 0-based inclusive line
130
+ range read fresh from disk, under TRUSTED framing that says the pointer was stale, that the
131
+ range may now hold DIFFERENT code than the symbol asked for, which lines arrived, and that
132
+ nothing was recorded as delivered. litectx refuses to slice a drifted range itself because an
133
+ UNLABELLED slice can silently be another symbol's body — the label is what makes it legal.
134
+ Detection is on the exported `StalePointerError` CLASS, never on message prose. A vanished
135
+ file or a range now entirely past the end is a refusal RESULT naming what happened, never a
136
+ throw. It **never touches the delivery ledger**: a slice is not the file, and a ledger that
137
+ recorded one would answer the next read with "you already have it" to a worker holding 90
138
+ lines of 900 — the untruthful-pointer failure the shim exists to prevent. The cost of that is
139
+ a later `shell_read` may re-deliver bytes the serve already showed; the shim over-delivers
140
+ rather than lies. A0 and A2 are untouched: with no capping arm the hook is not wired and
141
+ `ctx_get`'s contract is byte-identical.
142
+ - On the native surface the shim REPLACES the `NATIVE_READ_CAP` wrapper rather than layering over
143
+ it (both bound the same seam at the same 24KB, and a shim outside the native wrapper would
144
+ ledger already-truncated text as a whole file — the exact lie it exists to prevent). L2
145
+ (diff-on-change) is deliberately not built: it never fired on the real corpus.
146
+
8
147
  ## [0.12.0] — 2026-08-23
9
148
 
10
149
  ### Added (N4 slice 2 — the softgreen rung: a close that JUDGES, and a signer who has the last word)
package/README.md CHANGED
@@ -162,7 +162,7 @@ package, and gaps get fixed upstream rather than shimmed.
162
162
  bareloop is the productization of **[adaptlearn](https://github.com/hamr0/adaptlearn)**, a
163
163
  closed experimental record on whether agent-authored workflows improve under an honest gate.
164
164
  The design laws it inherited, and the open questions it has not answered yet, are in
165
- [`docs/01-product/PRD.md`](docs/01-product/PRD.md).
165
+ [`docs/product/PRD.md`](docs/product/PRD.md).
166
166
 
167
167
  ## License
168
168
 
@@ -26,7 +26,7 @@
26
26
  > where `hitl` IS admitted, `human-confirms` IS live, and the three `hitl-*` terminals exist —
27
27
  > integrate against what is written here, and expect the class-level rename when the next rung
28
28
  > lands. Records: PRD Addendum v1.71 and
29
- > `docs/02-features/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
29
+ > `docs/product/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
30
30
  > evidence F102–F105.
31
31
 
32
32
  ## What this is
@@ -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
@@ -1043,13 +1093,25 @@ raises it.
1043
1093
  of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
1044
1094
  and it is stated even on the pre-token reds, where the honest figure is a real `0`.
1045
1095
  `spendComplete` says whether that figure is EXACT; `false` means `spentUsd` is a FLOOR ("at
1046
- least $X", true total unknowable) and must not be read as a total. Four things set it, all
1096
+ least $X", true total unknowable) and must not be read as a total. Five things set it, all
1047
1097
  the same class — a call whose cost is unknowable from here: a round that came back unpriced
1048
1098
  (F6); a run that ABSORBED a stall, because the fuse's silent reissue may pay twice for one
1049
1099
  answer and the flag is one-way whatever the outcome; a `wall-halt` that cut a call
1050
- mid-flight (`cutMidCall`); and a RESUME whose fold was itself a floor
1100
+ mid-flight (`cutMidCall`); a RESUME whose fold was itself a floor
1051
1101
  (`priorSpendComplete: false` — an unknown does not heal by being carried forward, so every
1052
- round of this attempt being priced repairs nothing about the one before it). A wall stop read BETWEEN iterations or steps has no call in flight,
1102
+ round of this attempt being priced repairs nothing about the one before it); and a
1103
+ **transport retry** (F115, hamr's ruling: "one retry for transport layer failure and
1104
+ reporting with the rest end of gate"). Every worker Loop (scout, drafter, each step, the fix
1105
+ worker — the ONE seam in `planrun.js`) gets exactly ONE extra attempt, and only for a
1106
+ transport-class throw — fetch itself throwing with no HTTP response at all (a TLS fault,
1107
+ `ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause); an HTTP response
1108
+ (4xx/5xx/429) is never retried here and rides bare-agent's own unchanged policy. The budget is
1109
+ fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob, tighten-only
1110
+ doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase, attempt, error,
1111
+ recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw before any usage
1112
+ figure came back, so **any** transport retry floors `spendComplete` for the rest of the run even
1113
+ when the retry recovers and the run finishes green — the same one-way flag class as `stalled`
1114
+ and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
1053
1115
  so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
1054
1116
  field presence and never has to launder a missing `spentUsd` into `$0`.
1055
1117
 
@@ -1393,6 +1455,53 @@ reserved round bound, with the LAST round reserved for a toolless summary: the b
1393
1455
  a scout mid-tool-use, and text is its only deliverable, so without that reservation it
1394
1456
  spends its whole allowance exploring and reports nothing (F59 — measured on 15 of 18 runs).
1395
1457
 
1458
+ **Every cost figure bareloop reports is an ESTIMATE, not a billed amount (BA-21).** No
1459
+ vanilla LLM API returns a price — providers report TOKENS — so `spentUsd`, every
1460
+ `worker-round` cost, every halt readout and every ledger dollar is a token count
1461
+ multiplied by a RATE, never a provider invoice. Expect it to read somewhat high, and do
1462
+ not reconcile it against a bill expecting a match. There is deliberately **no per-model
1463
+ price table** behind it, and the reason is worth knowing before you decide how far to
1464
+ trust the number: a hand-curated table is a treadmill that rots SILENTLY — a price moves
1465
+ or a new model ships, the row goes stale, and the figure under-reports with no detector to
1466
+ say so. bare-agent instead matches a recognized Claude TIER (haiku / sonnet) by model id,
1467
+ and falls back to a conservative ceiling rate for anything it does not recognize. Both
1468
+ deliberately OVER-report rather than under-report, which is the fail-safe direction for a
1469
+ cap: a run halts slightly early rather than overspending unnoticed. **The consequence
1470
+ bites at the budget, and it is not softened here:** `budgetUsd` and `shellCapUsd` are
1471
+ enforced against these estimates, so an inaccurate rate is a cap that binds earlier or
1472
+ later than you intended. The cap is exact about the number it holds; the number is a
1473
+ guess.
1474
+
1475
+ **If you need accuracy, pass your own rates — the supported path, not a workaround.**
1476
+ bare-agent's `Loop` takes `rates: {in, out, cacheReadMult?, cacheWriteMult?}` — USD per 1K
1477
+ tokens, the two multipliers applying to the input rate for the cache-read and cache-write
1478
+ tiers (defaulting to Anthropic's 0.1× / 1.25×). A caller-supplied rate is recorded as
1479
+ VOUCHED rather than as a guess, and it silences bare-agent's own guesstimate warning.
1480
+ **Named, not papered over:** `runJob`/`runPlan` construct their own `Loop` and do not yet
1481
+ accept a `rates` passthrough, so today that option is reachable only by a caller driving
1482
+ bare-agent directly — a bareloop-run job is priced by the guesstimate, full stop. The
1483
+ passthrough is an open follow-up, and until it lands every paragraph above describes the
1484
+ only pricing a `runJob` adopter gets.
1485
+
1486
+ **The provenance is on the record, per round (`rateSource`).** The field arrives with
1487
+ bare-agent **>= 0.37**; under the pinned `^0.36.0` no provider payload carries it yet, so
1488
+ every priced round this library writes today reads UNKNOWN provenance — correctly, and by
1489
+ the same rule that governs the archive. (The one exception is the native per-turn
1490
+ `worker-turn`, whose `null` is bareloop's OWN statement rather than a forwarded one: that
1491
+ surface prices the SESSION, so a turn had no rate to guess.) Once the pin moves, every `worker-round` /
1492
+ `worker-turn` the plan flow writes carries bare-agent's own label beside `pricing`:
1493
+ `'provider'` (the provider reported its own authoritative cost — the native CLI surface)
1494
+ and `'caller'` (you passed the rate) are VOUCHED; `'tier'` (a recognized Claude tier,
1495
+ where **both** of bareloop's own production models land) and `'default'` (the blind
1496
+ ceiling) are GUESSES; `null` means nothing was priced, so there was no rate to have
1497
+ guessed. An ABSENT field is UNKNOWN provenance — every round archived before this signal
1498
+ existed, and it is never backfilled, never reconstructed, and never rounded up to vouched.
1499
+ Read it with `rateProvenance(record)` and `spendProvenance(events)` (`src/ledger.js`),
1500
+ whose predicate is an ALLOW-LIST of the two vouched values: anything else, including a
1501
+ value a future bare-agent adds, reads as a GUESS. **Reporting only** — `pricing` stays
1502
+ strictly two-valued (`'priced'`|`'unpriced'`), the `pricing-red` halt still keys on cost
1503
+ alone, and nothing about provenance decides, halts or refuses anything.
1504
+
1396
1505
  **What the worker is told (tool mode):** the absolute repository root (F10 — bare-agent's
1397
1506
  shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
1398
1507
  blind), the close's CURRENT output as the tree's state (F13 — never framed as "your previous
@@ -1977,9 +2086,62 @@ the same refusal emits, so counting the escalation too would double every refusa
1977
2086
  `suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
1978
2087
  status rows (`open → filed → fixed → consumed`) are human-appended, and the fold shows
1979
2088
  the latest per key. Pure pieces exported for custom folds: `classifyIncidents(events,
1980
- {spine?})`, `foldLedger(rows)`, `ledgerDeltas(fold, occurrences)`. CLI lands at N5; the
2089
+ {spine?})`, `foldLedger(rows)`, `ledgerDeltas(fold, occurrences)`. Riding with them,
2090
+ and deliberately NOT a ledger class: `rateProvenance(record)` → `vouched|guessed|unpriced|unknown`
2091
+ and `spendProvenance(events)` → per-provenance `{rounds, usd, unpricedRounds}` buckets answer
2092
+ "how much of this run's spend was priced by a rate nobody vouched for" (BA-21 — see *Every cost
2093
+ figure bareloop reports is an ESTIMATE* above). A bucket's `usd` sums FINITE costs only and is a
2094
+ FLOOR whenever its `unpricedRounds` is non-zero (F6 — an unknown cost is counted, never summed as
2095
+ $0). `VOUCHED_RATE_SOURCES` is the frozen allow-list they key on. **Reporting only**: no incident,
2096
+ no class, no halt. CLI lands at N5; the
1981
2097
  panel reads the same file at N6.
1982
2098
 
2099
+ ### `runBehaviour(events, { runId? })` / `formatBehaviour(summary)` — `src/behaviour.js`
2100
+
2101
+ Build-list item 2 (agreed 2026-08-23): one report-only text block summarizing what a run
2102
+ DID with its tools, computed entirely from the run's own gate-audit JSONL. **Gates nothing,
2103
+ changes no verdict, writes NOTHING to the spine** — a pure function over records that already
2104
+ exist, same posture as `spendProvenance` above. `runBehaviour` counts and returns the summary
2105
+ object; `formatBehaviour` renders it as the printed block:
2106
+
2107
+ ```
2108
+ 94 tool calls · 61 read, 28 grep, 5 recall
2109
+ 17 exact repeats (~18%)
2110
+ ```
2111
+
2112
+ `llm` action rows are provider rounds, not tool calls, and are always excluded; only
2113
+ `decision: 'allow'` rows count toward the total (a `deny` is tallied separately as `N denied`,
2114
+ shown only when nonzero). **The exact-repeat key is the FULL recorded action — `type` + `path`
2115
+ + every key of `args`** (sorted, stably serialized), never collapsed to `{type, path}`: if the
2116
+ gate ever starts recording byte ranges or offsets, two different slices of one file must not
2117
+ read as the same repeated call. Repeat detection is therefore exactly as sharp as what the gate
2118
+ records, no sharper — a tool that reads identical bytes through two differently-shaped argument
2119
+ objects will not be caught by this. `scripts/behaviour-readout.mjs <gate-audit.jsonl> [run_id]`
2120
+ prints the block for one run or every run_id found in the file, skipping malformed lines with
2121
+ a count rather than throwing.
2122
+
2123
+ ```js
2124
+ import { runBehaviour, formatBehaviour } from 'bareloop';
2125
+ const events = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
2126
+ console.log(formatBehaviour(runBehaviour(events, { runId })));
2127
+ ```
2128
+
2129
+ #### `memory-cache` — the read shim's own end-of-run readout (`src/readshim.js`, `src/planrun.js`)
2130
+
2131
+ A single spine record, emitted once by `runPlan` right before it returns (so it lands on the
2132
+ spine before the caller's `job-end`), reporting what the READ SHIM (L1 pointer + L4 cap) saved
2133
+ this run: `{pointered, capped, bytesWithheld, approxTokens}`. `pointered` counts re-reads of
2134
+ unchanged content answered with a pointer instead of the bytes; `capped` counts reads served as
2135
+ a bounded slice; `bytesWithheld` sums exactly the bytes NOT re-sent in both cases (never
2136
+ estimated) — the diff lever (L2) isn't counted, it's a different response shape. `approxTokens`
2137
+ is `Math.round(bytesWithheld / 4)`, a bytes→tokens ESTIMATE named accordingly so nothing reads
2138
+ it as measured. **Emitted only when the shim is armed** (any `readShim` arm but the off one) —
2139
+ an unarmed run emits no record at all, never a fabricated zero. It carries no cost fields, so it
2140
+ is not a spend record and no spend-slicing instrument needs to account for it.
2141
+ `scripts/run-u.mjs` prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
2142
+ (`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
2143
+ prints the same line when handed the run's spine file as its optional third positional.
2144
+
1983
2145
  ## Architecture
1984
2146
 
1985
2147
  Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
@@ -2011,7 +2173,7 @@ is disclosed to the authoring agent; only admitted verbs are callable per job.
2011
2173
  ## Gotchas
2012
2174
 
2013
2175
  *TBD from real adopter friction; recorded here as they're found (repo-side friction goes
2014
- to `docs/FINDINGS.md`).*
2176
+ to `docs/logs/FINDINGS.md`).*
2015
2177
 
2016
2178
  ## Constraints
2017
2179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.12.0",
3
+ "version": "0.14.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.36.0",
26
+ "bare-agent": "^0.39.0",
27
27
  "bareguard": "^0.13.0",
28
28
  "litectx": "^0.32.0"
29
29
  },
@@ -0,0 +1,150 @@
1
+ // The run behaviour summary (PRD build-list §2, agreed 2026-08-23): ONE
2
+ // report-only text block computed entirely from a run's gate-audit JSONL.
3
+ // It gates NOTHING, mints no verdict, writes NOTHING to the spine — a pure
4
+ // read of records that already exist. "We can see what a run cost, never
5
+ // what it wasted" is the gap this closes; item 4's five-minute-replay test
6
+ // gets this as its cheap first slice.
7
+ //
8
+ // Gate-audit rows this reads (observed shape, this repo's real archive):
9
+ // {ts, seq, run_id, phase, action: {type, path?, args?}, decision, ...}
10
+ // `action.type` is one of `llm` (a provider round, NOT a tool call — always
11
+ // excluded), `read`, `edit`, `ctx_remember`. `action.args.tool` names the
12
+ // concrete tool for read-shaped actions (`shell_read`, `shell_grep`,
13
+ // `ctx_recall`, `ctx_get`, `ctx_recent`, `ctx_impact`, `ctx_related`);
14
+ // edit/ctx_remember rows carry no `args.tool`. `decision` is `allow` for a
15
+ // gated tool call, `null` for an llm row, or `deny`.
16
+
17
+ /**
18
+ * Stable JSON — keys sorted at every level — so two calls with the same
19
+ * arguments in a different insertion order (or a different property visit
20
+ * order out of JSON.parse) hash to the same string. Used only to build the
21
+ * exact-repeat key below; it is not a general-purpose canonicalizer.
22
+ * @param {any} v
23
+ * @returns {string}
24
+ */
25
+ function stableStringify(v) {
26
+ if (v === null || typeof v !== 'object') return JSON.stringify(v);
27
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(',')}]`;
28
+ const keys = Object.keys(v).sort();
29
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(',')}}`;
30
+ }
31
+
32
+ /**
33
+ * The exact-repeat key for one tool-call action: `type` + `path` + a stable
34
+ * serialization of the WHOLE `args` object.
35
+ *
36
+ * Load-bearing, and deliberately not `{type, path}`: this repo's own history
37
+ * is a blind-instrument catalogue of collapsed keys reading two DIFFERENT
38
+ * things as the same thing (a ledger that summed input+output with no cache
39
+ * tiers, a gate audit that collapsed reads to `{type,path}`, `git status` as
40
+ * "did it write?"). If the gate ever starts recording byte ranges or offsets
41
+ * in `args`, two different slices of the same file must NOT count as a
42
+ * repeat — dropping `args` from the key would silently manufacture that
43
+ * exact confound here. This detector is exactly as sharp as what the gate
44
+ * records, no sharper: a tool that reads the same bytes through two
45
+ * differently-shaped argument objects will not be caught, and that is an
46
+ * honest limit, not a bug.
47
+ * @param {{type?: string, path?: string, args?: any}} action
48
+ * @returns {string}
49
+ */
50
+ function repeatKey(action) {
51
+ return `${action?.type ?? ''} ${action?.path ?? ''} ${stableStringify(action?.args ?? null)}`;
52
+ }
53
+
54
+ /**
55
+ * The display label for one tool-call breakdown row. Kept SEPARATE from the
56
+ * counting key (`args.tool || action.type`, computed inline in
57
+ * {@link runBehaviour}): the summary object stores the raw tool name so a
58
+ * consumer can trace a count back to its source, and only {@link
59
+ * formatBehaviour} shortens it for the printed line — the agreed shape reads
60
+ * "read"/"grep"/"recall", not "shell_read"/"shell_grep"/"ctx_recall".
61
+ * @param {string} key
62
+ * @returns {string}
63
+ */
64
+ function displayLabel(key) {
65
+ return key.replace(/^shell_/, '').replace(/^ctx_/, '');
66
+ }
67
+
68
+ /**
69
+ * Summarize what a run DID with its tools, from its gate-audit records
70
+ * alone. Pure and read-only: no IO, no spine writes, no verdict.
71
+ *
72
+ * Counting rules:
73
+ * - `llm` rows are provider rounds, never tool calls — always excluded.
74
+ * - a tool call counts toward `totalCalls`/`byTool`/`uniquePaths`/repeats
75
+ * only when `decision === 'allow'`; a `deny` is tallied separately in
76
+ * `denied` and touches nothing else (denied ≠ done).
77
+ * - `byTool` is keyed by `action.args.tool` when present, else
78
+ * `action.type` (an edit or ctx_remember has no `args.tool`).
79
+ * - exact repeats: see {@link repeatKey}. A first occurrence of an action
80
+ * is not itself a "repeat" — only the 2nd, 3rd, ... occurrences count,
81
+ * so `repeats` can never exceed `totalCalls - byTool-group-count`.
82
+ * @param {Array<{run_id?: string, action?: {type?: string, path?: string, args?: any}, decision?: string|null}>} events
83
+ * @param {{runId?: string}} [opts]
84
+ * @returns {{totalCalls: number, byTool: Record<string, number>, denied: number,
85
+ * uniquePaths: number, repeats: number, repeatPct: number|null}}
86
+ */
87
+ export function runBehaviour(events, { runId } = {}) {
88
+ const list = Array.isArray(events) ? events : [];
89
+ const scoped = runId === undefined ? list : list.filter((e) => e?.run_id === runId);
90
+
91
+ const byTool = /** @type {Record<string, number>} */ ({});
92
+ const paths = new Set();
93
+ const keyCounts = new Map();
94
+ let totalCalls = 0;
95
+ let denied = 0;
96
+
97
+ for (const e of scoped) {
98
+ const action = e?.action ?? {};
99
+ if (action.type === 'llm') continue; // a provider round, never a tool call
100
+ if (e?.decision === 'deny') { denied += 1; continue; }
101
+ if (e?.decision !== 'allow') continue; // neither allow nor deny: not a counted call
102
+
103
+ totalCalls += 1;
104
+ const tool = action.args?.tool ?? action.type ?? 'unknown';
105
+ byTool[tool] = (byTool[tool] ?? 0) + 1;
106
+ if (action.path) paths.add(action.path);
107
+
108
+ const k = repeatKey(action);
109
+ keyCounts.set(k, (keyCounts.get(k) ?? 0) + 1);
110
+ }
111
+
112
+ let repeats = 0;
113
+ for (const count of keyCounts.values()) if (count > 1) repeats += count - 1;
114
+
115
+ return {
116
+ totalCalls,
117
+ byTool,
118
+ denied,
119
+ uniquePaths: paths.size,
120
+ repeats,
121
+ repeatPct: totalCalls > 0 ? Math.round((repeats / totalCalls) * 100) : null,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Render a {@link runBehaviour} summary as the agreed printable block
127
+ * (2–4 short lines). A zero-call run formats plainly ("0 tool calls") rather
128
+ * than a NaN-laced percentage — an empty run is a complete, reportable
129
+ * answer, not a division error.
130
+ * @param {ReturnType<typeof runBehaviour>} summary
131
+ * @returns {string}
132
+ */
133
+ export function formatBehaviour(summary) {
134
+ const { totalCalls, byTool, denied, repeats, repeatPct } = summary; // uniquePaths: data-only, not in the agreed printed shape
135
+ const lines = [];
136
+
137
+ if (totalCalls === 0) {
138
+ lines.push('0 tool calls');
139
+ } else {
140
+ const breakdown = Object.entries(byTool)
141
+ .sort((a, b) => b[1] - a[1])
142
+ .map(([tool, n]) => `${n} ${displayLabel(tool)}`)
143
+ .join(', ');
144
+ lines.push(`${totalCalls} tool call${totalCalls === 1 ? '' : 's'} · ${breakdown}`);
145
+ lines.push(`${repeats} exact repeat${repeats === 1 ? '' : 's'} (~${repeatPct}%)`);
146
+ }
147
+ if (denied > 0) lines.push(`${denied} denied`);
148
+
149
+ return lines.join('\n');
150
+ }
package/src/bridges.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Layer 3 — the REUSE registry (design record:
2
- // docs/02-features/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
2
+ // docs/product/2026-08-01-layer-3-reuse-design.md, D1/D2/D6, frozen 2026-08-01).
3
3
  //
4
4
  // A BRIDGE is the plan a green actually executed, kept so the next run of the same
5
5
  // SHAPE starts from it instead of cold. This module is the box and the label on it:
package/src/calibrate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // THE CALIBRATION GATE — softgreen module 5 (design record
2
- // docs/02-features/2026-08-17-softgreen-review-door-design.md §4.4, the
2
+ // docs/product/2026-08-17-softgreen-review-door-design.md §4.4, the
3
3
  // 2026-08-18 threshold addendum rulings 1–3, and the second same-day addendum's
4
4
  // rulings 1 and 3).
5
5
  //
package/src/cardauthor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // THE TWO SIGNED ARTIFACTS — softgreen module 4 (design record
2
- // docs/02-features/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
2
+ // docs/product/2026-08-17-softgreen-review-door-design.md §4.3 and §4.4,
3
3
  // plus the 2026-08-18 threshold addendum).
4
4
  //
5
5
  // Module 1 built the judged floor's core, module 2 wired the stage into a close,
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
- export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from './ledger.js';
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).
@@ -213,6 +228,11 @@ export {
213
228
  HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED, HUMAN_CHECKPOINTS,
214
229
  } from './declaredclose.js';
215
230
  export { closeStagesOf } from './plan.js';
231
+ // Build-list item 2 (2026-08-23) — the run behaviour summary: a report-only
232
+ // read of a run's gate-audit records. Gates nothing, mints no verdict,
233
+ // writes nothing to the spine; `runBehaviour` computes, `formatBehaviour`
234
+ // renders the printable block.
235
+ export { runBehaviour, formatBehaviour } from './behaviour.js';
216
236
  // M4b — the interview, the refusal, the composition, and D9's three gates.
217
237
  // `prepareSigning` returns the resolved spec's HASH and the seed evidence; it
218
238
  // never signs — the approvals array and the human's word are unchanged.
package/src/job.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Job-spec validator — the OPERATOR-owned half of the config story (N1,
2
- // design record: docs/02-features/2026-07-12-n1-job-close-schema-design.md).
2
+ // design record: docs/product/2026-07-12-n1-job-close-schema-design.md).
3
3
  // A job spec is the arbiter's rulebook: the close chain, the budget, the
4
4
  // outer write fence, the escalation route. It is pure declarative data —
5
5
  // no freeform code is expressible anywhere in it — and it validates
package/src/judged.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // THE JUDGED FLOOR'S CORE — softgreen module 1 (design record
2
- // docs/02-features/2026-08-17-softgreen-review-door-design.md §4, plus the
2
+ // docs/product/2026-08-17-softgreen-review-door-design.md §4, plus the
3
3
  // 2026-08-18 threshold addendum). POC: poc/softgreen-judge/poc.mjs — validated
4
4
  // logic, rewritten here, never copied (graduation is a rewrite).
5
5
  //