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 +139 -0
- package/README.md +1 -1
- package/bareloop.context.md +169 -7
- package/package.json +2 -2
- package/src/behaviour.js +150 -0
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +1 -1
- package/src/index.js +21 -1
- package/src/job.js +1 -1
- package/src/judged.js +1 -1
- package/src/ledger.js +103 -1
- package/src/plan.js +43 -2
- package/src/planrun.js +306 -16
- package/src/readshim.js +570 -0
- package/src/reuse.js +27 -3
- package/src/root.js +1 -1
- package/src/run.js +39 -4
- package/src/selection.js +1 -1
- package/src/tools.js +34 -2
- package/src/transport.js +71 -0
- package/types/behaviour.d.ts +46 -0
- package/types/index.d.ts +3 -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 +128 -0
- package/types/reuse.d.ts +24 -0
- package/types/run.d.ts +15 -1
- package/types/tools.d.ts +17 -1
- package/types/transport.d.ts +19 -0
package/src/ledger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The upstream ledger (module 4) — reactive lib/primitive incident monitoring.
|
|
2
|
-
// Design record: docs/
|
|
2
|
+
// Design record: docs/product/2026-07-11-upstream-ledger-design.md (adaptlearn
|
|
3
3
|
// POC'd spec) + its 2026-07-13 addendum: the bareloop event mapping. One
|
|
4
4
|
// append-only JSONL both the consumer (the panel, N6) and the maintainer read;
|
|
5
5
|
// the A1/A2/A3 upstream-ask flow, mechanized — evidence in, human judgment out.
|
|
@@ -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)
|