bareloop 0.6.0 → 0.7.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 +407 -0
- package/README.md +28 -11
- package/bareloop.context.md +510 -28
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/index.js +29 -0
- package/src/ladder.js +196 -0
- package/src/ledger.js +27 -0
- package/src/plan.js +75 -7
- package/src/planrun.js +762 -146
- package/src/ralph.js +148 -28
- package/src/reuse.js +1602 -0
- package/src/run.js +73 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/index.d.ts +3 -0
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +18 -3
- package/types/planrun.d.ts +108 -9
- package/types/ralph.d.ts +82 -4
- package/types/reuse.d.ts +388 -0
- package/types/run.d.ts +50 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
package/src/ralph.js
CHANGED
|
@@ -118,7 +118,12 @@ function spawnClose(cmd, args, { env, cwd, timeoutMs }) {
|
|
|
118
118
|
* Two rules, deliberately kept BOTH ways round:
|
|
119
119
|
* - `names` / `prefixes` — the knowns. A literal denylist is a chase (every
|
|
120
120
|
* new provider mints a new variable), so it is not load-bearing on its own;
|
|
121
|
-
* it exists for names the shape rule cannot see
|
|
121
|
+
* it exists for names the shape rule STRUCTURALLY cannot see — either no
|
|
122
|
+
* `_`-bounded credential suffix at all (`AWS_ACCESS_KEY_ID`, `PGPASSWORD`,
|
|
123
|
+
* `MYSQL_PWD`), a `*_KEY` that is not `*_API_KEY` (`SSH_PRIVATE_KEY`), or a
|
|
124
|
+
* BARE spelling with nothing in front of the suffix (`TOKEN`, `SECRET`).
|
|
125
|
+
* Names are matched EXACTLY, which is what keeps connection config out of
|
|
126
|
+
* the strip (`PGHOST`, `SECRETS_DIR`, `TOKENIZERS_PARALLELISM` all survive).
|
|
122
127
|
* - `shape` — the NAME-SHAPE rule, which is what bounds the chase: anything
|
|
123
128
|
* whose name ends in the credential suffixes goes, whoever minted it and
|
|
124
129
|
* whether or not anyone here has heard of it. Case-insensitive; no `g` flag,
|
|
@@ -130,8 +135,18 @@ function spawnClose(cmd, args, { env, cwd, timeoutMs }) {
|
|
|
130
135
|
*/
|
|
131
136
|
export const CLOSE_ENV_DENY = Object.freeze({
|
|
132
137
|
names: Object.freeze([
|
|
138
|
+
// provider keys
|
|
133
139
|
'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY',
|
|
134
140
|
'ANTHROPIC_AUTH_TOKEN', 'GITHUB_TOKEN', 'GH_TOKEN', 'NPM_TOKEN',
|
|
141
|
+
// credential spellings the shape rule structurally cannot see, because their
|
|
142
|
+
// names carry no `_`-bounded suffix (operator ruling 2026-07-31, "add", on a
|
|
143
|
+
// delta-review finding that MEASURED every one of these reaching the child).
|
|
144
|
+
// Documented tools' own variables, not invented ones:
|
|
145
|
+
'PGPASSWORD', 'PGPASSFILE', 'PGSSLKEY', // libpq (`PGPASS` is NOT a libpq variable — the FILE is ~/.pgpass, named by PGPASSFILE)
|
|
146
|
+
'MYSQL_PWD', 'REDISCLI_AUTH', // mysql client, redis-cli
|
|
147
|
+
'STRIPE_KEY', 'STRIPE_SECRET_KEY', // `*_KEY` is not `*_API_KEY`: the shape rule misses it
|
|
148
|
+
'SSH_PRIVATE_KEY', 'PRIVATE_KEY', 'SECRET_KEY',
|
|
149
|
+
'TOKEN', 'SECRET', 'API_KEY', 'PASSWORD', 'PASSWD', // the bare spellings; a suffix rule needs a prefix to bound
|
|
135
150
|
]),
|
|
136
151
|
prefixes: Object.freeze(['AWS_']),
|
|
137
152
|
shape: /(_API_KEY|_SECRET|_TOKEN|_PASSWORD|_CREDENTIALS?)$/i,
|
|
@@ -408,6 +423,27 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
408
423
|
},
|
|
409
424
|
});
|
|
410
425
|
|
|
426
|
+
/**
|
|
427
|
+
* The exhaustion-by-PROGRESS seam, as a contract rather than "whatever one module
|
|
428
|
+
* happens to return". Two governors implement it and they read different signals —
|
|
429
|
+
* the step ladder's repeats-and-writes (src/ladder.js) and the close-fix loop's
|
|
430
|
+
* per-stage graded numbers (src/trend.js) — which is the whole reason ralph takes an
|
|
431
|
+
* interface here and interprets nothing itself.
|
|
432
|
+
*
|
|
433
|
+
* `record` returns the governor's own reading, which ralph emits verbatim and never
|
|
434
|
+
* inspects; each reading names its `governor` so two instruments under one spine type
|
|
435
|
+
* can never be averaged into one number. `report` is the only shape ralph itself
|
|
436
|
+
* reads. `terminal` is optional and overrides the exhaustion PROSE only — never the
|
|
437
|
+
* category, never the outcome.
|
|
438
|
+
*
|
|
439
|
+
* @typedef {object} LoopGovernor
|
|
440
|
+
* @property {(o: {iteration: number, gap?: string}) => object} record
|
|
441
|
+
* @property {() => boolean} struckOut
|
|
442
|
+
* @property {() => {strikes: number, limit: number, iterations: number, distinctGaps?: number}} report
|
|
443
|
+
* @property {() => string} brief
|
|
444
|
+
* @property {() => {decision: string, options: string[]}} [terminal]
|
|
445
|
+
*/
|
|
446
|
+
|
|
411
447
|
/**
|
|
412
448
|
* The loop: `while close-red and under-cap: run the middle`. Stops at first
|
|
413
449
|
* green (within-run tuning past a visible close is the fit-to-pass surface,
|
|
@@ -433,7 +469,28 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
433
469
|
* zone, worker-crash routing (F32), and the cap taxonomy apply unchanged.
|
|
434
470
|
* Injected by run.js only; inexpressible in any config or plan — the agent
|
|
435
471
|
* never authors its judge any more than its close.
|
|
436
|
-
* @param {number} opts.capRuns budget: max middle runs
|
|
472
|
+
* @param {number} [opts.capRuns] budget: max middle runs. Required UNLESS `ladder`
|
|
473
|
+
* is supplied — the two exhaustion rules are alternatives, never both at once.
|
|
474
|
+
* The "unless" is ENFORCED at entry (a param guard that throws), not merely
|
|
475
|
+
* documented: an optional-in-the-contract bound with nothing checking it made a
|
|
476
|
+
* loop that never ran report a governance stop that never happened.
|
|
477
|
+
* @param {LoopGovernor} [opts.ladder]
|
|
478
|
+
* the PROGRESS governor (src/ladder.js) that replaces the fixed count for a plan
|
|
479
|
+
* STEP's micro-loop. With it, the loop is bounded by strikes rather than by a
|
|
480
|
+
* number of runs: iterations float, and money, the wall, the variance meter and
|
|
481
|
+
* the stall fuse keep every bit of their current authority (each still ends the
|
|
482
|
+
* loop through the middle's own throw). Ralph still interprets nothing — it hands
|
|
483
|
+
* the ladder the iteration's gap and reads back a boolean.
|
|
484
|
+
*
|
|
485
|
+
* The CLOSE-FIX loop now supplies one too (PRD v1.46 §4, once its own $0 replay
|
|
486
|
+
* over all 8 archived fix loops came back clean: 0 greens harmed, 1 waste case
|
|
487
|
+
* caught). Its governor reads a different signal — the close's own per-stage
|
|
488
|
+
* graded numbers (src/trend.js), not repeats and writes — which is exactly why
|
|
489
|
+
* this is a SEAM and not a hard-coded rule: ralph still interprets nothing.
|
|
490
|
+
* An optional `terminal()` on the governor overrides the exhaustion prose, because
|
|
491
|
+
* the step ladder's copy offers the human a replan and there is no planner at the
|
|
492
|
+
* close. The category and the outcome are NOT overridable: one exhaustion terminal,
|
|
493
|
+
* two triggers.
|
|
437
494
|
* @param {(type: string, data?: object) => object} opts.emit a spine emitter
|
|
438
495
|
* @param {(s: string) => string} [opts.redact] source scrubber for close output
|
|
439
496
|
* (secrets never enter the spine); injected so the shell stays stdlib-only
|
|
@@ -452,8 +509,30 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
452
509
|
* with zero writes, a crash stays what it always was: an instrument stop.
|
|
453
510
|
* @returns {Promise<'green'|'escalated'>}
|
|
454
511
|
*/
|
|
455
|
-
export async function ralph({ middle, close, judge, capRuns, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }) {
|
|
456
|
-
|
|
512
|
+
export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites }) {
|
|
513
|
+
// PARAM GUARD (the BA-4 class — a caller's malformed argument, which is the one
|
|
514
|
+
// thing this shell throws for; a WORKER's bad reach comes back as a refusal
|
|
515
|
+
// RESULT instead, and that rule is about feedback, not about the API contract).
|
|
516
|
+
// `capRuns` became optional in this signature when `ladder` arrived, and nothing
|
|
517
|
+
// enforced the "unless". MEASURED with neither supplied: `1 <= undefined` is
|
|
518
|
+
// false, so the middle ran 0 times and ralph still emitted `cap-halt` and an
|
|
519
|
+
// escalation reading `undefined/undefined runs spent, close still red` — a
|
|
520
|
+
// governance stop narrated for a run that never happened, on the public export.
|
|
521
|
+
// A bound that reports halting a loop it never entered is exactly the
|
|
522
|
+
// blind-instrument class this repo keeps paying for, so the malformed call dies
|
|
523
|
+
// at the door instead of minting a record. The message names BOTH alternatives:
|
|
524
|
+
// either one repairs the call, and naming one would send the caller to the wrong
|
|
525
|
+
// repair.
|
|
526
|
+
if (!ladder && !(Number.isInteger(capRuns) && /** @type {number} */ (capRuns) > 0)) {
|
|
527
|
+
// quoted only for a string, so `"3"` reads as the type error it is while
|
|
528
|
+
// `NaN`/`undefined` read as themselves (JSON.stringify renders both as a lie)
|
|
529
|
+
const got = typeof capRuns === 'string' ? JSON.stringify(capRuns) : String(capRuns);
|
|
530
|
+
throw new Error(`ralph: capRuns must be a positive integer (got ${got}) unless a \`ladder\` governor is supplied — they are alternative exhaustion rules and a loop with neither cannot stop honestly`);
|
|
531
|
+
}
|
|
532
|
+
emit('run-start', {
|
|
533
|
+
...(ladder ? { strikeLimit: ladder.report().limit } : { capRuns }),
|
|
534
|
+
close: judge ? 'judge:injected' : /** @type {string[]} */ (close).join(' '),
|
|
535
|
+
});
|
|
457
536
|
// The blind spot is NAMED, never hidden: with no judgment-rendered signal this
|
|
458
537
|
// close cannot tell a crash from an honest red (they are byte-identical at the
|
|
459
538
|
// exit-code seam), so the record says so out loud rather than passing the exit
|
|
@@ -463,7 +542,22 @@ export async function ralph({ middle, close, judge, capRuns, emit, redact, close
|
|
|
463
542
|
if (!judged && !judge) emit('close-unaudited', { close: /** @type {string[]} */ (close).join(' '), meaning: 'no judgment-rendered signal declared — a crash-at-load is indistinguishable from an honest red for this close' });
|
|
464
543
|
const verdicts = [];
|
|
465
544
|
let gap;
|
|
466
|
-
|
|
545
|
+
// ONE exhaustion terminal, two triggers — the same three records in the same
|
|
546
|
+
// order either way, so the count path and the ladder path cannot drift into
|
|
547
|
+
// reporting the stop differently. The CATEGORY is `cap-halt` for both, and
|
|
548
|
+
// deliberately: downstream counts escalation categories against an executable
|
|
549
|
+
// excluded-set (ledger.js), and the step loop routes its ONE replan off exactly
|
|
550
|
+
// this name. Only the trigger changed; the taxonomy did not.
|
|
551
|
+
/** @param {number} iteration @param {object} halt @param {object} spend @param {string} decision @param {string[]} options */
|
|
552
|
+
const exhausted = (iteration, halt, spend, decision, options) => {
|
|
553
|
+
emit('cap-halt', { category: 'cap-halt', meaning: 'not under cap — not "can\'t"', ...halt });
|
|
554
|
+
emit('escalation', { category: 'cap-halt', decisionReady: true, verdicts, spend, decision, options });
|
|
555
|
+
emit('run-end', { outcome: 'escalated', iterations: iteration });
|
|
556
|
+
return /** @type {'escalated'} */ ('escalated');
|
|
557
|
+
};
|
|
558
|
+
// With a ladder the count is not the bound: the loop runs until the ladder strikes
|
|
559
|
+
// out, or until the middle throws (money, wall, variance, stall — all unchanged).
|
|
560
|
+
for (let iteration = 1; ladder || iteration <= /** @type {number} */ (capRuns); iteration++) {
|
|
467
561
|
emit('iteration-start', { iteration });
|
|
468
562
|
try {
|
|
469
563
|
await middle(iteration, gap);
|
|
@@ -554,30 +648,56 @@ export async function ralph({ middle, close, judge, capRuns, emit, redact, close
|
|
|
554
648
|
gap = `Your edit CRASHED the test suite — it can no longer even load and judge (${v.detail}). `
|
|
555
649
|
+ `Files you have written this run: ${shown.join(', ')}${more > 0 ? ` (and ${more} more)` : ''}. `
|
|
556
650
|
+ 'Fix or revert your change so the suite can run again — nothing can pass while the suite cannot load.';
|
|
557
|
-
|
|
651
|
+
} else {
|
|
652
|
+
// Forbidden zone: no judgment was rendered, so there is no verdict. Escalate
|
|
653
|
+
// by the outcome's OWN name and never retry — retrying a broken arbiter is
|
|
654
|
+
// the §5b violation adaptlearn found live in its shipped shell (F25/Z-3).
|
|
655
|
+
const fault = Object.hasOwn(CLOSE_FAULTS, v.verdict) ? CLOSE_FAULTS[v.verdict] : undefined;
|
|
656
|
+
if (fault) {
|
|
657
|
+
emit('escalation', {
|
|
658
|
+
category: fault.category, decisionReady: true, verdicts,
|
|
659
|
+
spend: { runs: iteration, capRuns },
|
|
660
|
+
decision: fault.decision, options: fault.options, detail: v.detail,
|
|
661
|
+
});
|
|
662
|
+
emit('run-end', { outcome: 'escalated', iterations: iteration });
|
|
663
|
+
return 'escalated';
|
|
664
|
+
}
|
|
665
|
+
gap = v.gap; // feedback for the next middle run; the shell itself learns nothing
|
|
558
666
|
}
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
667
|
+
// ── the ladder reads THIS red iteration, whichever gap it ended up with (a
|
|
668
|
+
// worker-crash gap is feedback the attempt really received, so it is a reading
|
|
669
|
+
// like any other). ONE call site, after both branches have settled `gap`: two
|
|
670
|
+
// would let a future branch quietly opt out of the bound.
|
|
671
|
+
if (ladder) {
|
|
672
|
+
const r = ladder.record({ iteration, gap });
|
|
673
|
+
emit('ladder', r);
|
|
674
|
+
if (ladder.struckOut()) {
|
|
675
|
+
const rep = ladder.report();
|
|
676
|
+
// The governor may name its own decision/options; it may NOT name its own
|
|
677
|
+
// category or outcome. A stop is spelled the same either way (`cap-halt`,
|
|
678
|
+
// 'escalated') because downstream counts categories against an executable
|
|
679
|
+
// excluded-set — only the PROSE a human reads is governor-specific.
|
|
680
|
+
const term = typeof ladder.terminal === 'function' ? ladder.terminal() : null;
|
|
681
|
+
return exhausted(iteration,
|
|
682
|
+
{
|
|
683
|
+
strikes: rep.strikes,
|
|
684
|
+
strikeLimit: rep.limit,
|
|
685
|
+
iterations: rep.iterations,
|
|
686
|
+
...(rep.distinctGaps === undefined ? {} : { distinctGaps: rep.distinctGaps }),
|
|
687
|
+
},
|
|
688
|
+
{ runs: iteration, strikes: rep.strikes, strikeLimit: rep.limit },
|
|
689
|
+
term?.decision ?? `${rep.strikes}/${rep.limit} strikes — the step stopped making progress. ${ladder.brief()} Continue, change approach, or stop?`,
|
|
690
|
+
term?.options ?? ['let the planner re-allocate what is left (replan)', 'change approach', 'abandon the task']);
|
|
691
|
+
}
|
|
571
692
|
}
|
|
572
|
-
gap = v.gap; // feedback for the next middle run; the shell itself learns nothing
|
|
573
693
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
694
|
+
// The FIXED-COUNT exhaustion, reached only without a ladder. Both callers inside this
|
|
695
|
+
// package now supply one (the step loop's `createLadder`, the close-fix loop's
|
|
696
|
+
// progress governor), so the only route left is the public export — kept because it IS
|
|
697
|
+
// the public export, and because a `capRuns` bound with no reachable exhaustion would
|
|
698
|
+
// be a bound in name only.
|
|
699
|
+
const spent = /** @type {number} */ (capRuns);
|
|
700
|
+
return exhausted(spent, { capRuns }, { runs: spent, capRuns },
|
|
701
|
+
`${spent}/${spent} runs spent, close still red. Continue, change approach, or stop?`,
|
|
702
|
+
['raise the cap and rerun', 'change the middle/harness', 'abandon the task']);
|
|
583
703
|
}
|