bareloop 0.5.0 → 0.6.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 +746 -1
- package/README.md +13 -3
- package/bareloop.context.md +311 -234
- package/package.json +3 -3
- package/src/clock.js +181 -0
- package/src/exits.js +8 -2
- package/src/index.js +23 -17
- package/src/job.js +231 -108
- package/src/ledger.js +28 -3
- package/src/plan.js +356 -20
- package/src/planrun.js +978 -98
- package/src/ralph.js +226 -7
- package/src/root.js +32 -12
- package/src/run.js +61 -390
- package/src/stall.js +252 -0
- package/src/tools.js +611 -0
- package/src/validate.js +35 -250
- package/types/clock.d.ts +109 -0
- package/types/index.d.ts +4 -6
- package/types/job.d.ts +83 -8
- package/types/ledger.d.ts +3 -1
- package/types/plan.d.ts +107 -7
- package/types/planrun.d.ts +51 -3
- package/types/ralph.d.ts +62 -4
- package/types/root.d.ts +10 -2
- package/types/run.d.ts +9 -17
- package/types/stall.d.ts +80 -0
- package/types/tools.d.ts +262 -0
- package/types/validate.d.ts +21 -38
- package/src/extract.js +0 -103
- package/src/interpret.js +0 -887
- package/types/extract.d.ts +0 -25
- package/types/interpret.d.ts +0 -201
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,9 +23,9 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build:types"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"bare-agent": "^0.
|
|
26
|
+
"bare-agent": "^0.35.0",
|
|
27
27
|
"bareguard": "^0.12.0",
|
|
28
|
-
"litectx": "^0.
|
|
28
|
+
"litectx": "^0.31.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.0.0",
|
package/src/clock.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// T's wall clock — the shell's time instrument (PRD v1.27/v1.29; materials
|
|
2
|
+
// design record 2026-07-26, addenda 1 and 3).
|
|
3
|
+
//
|
|
4
|
+
// Time is arbiter territory with exactly `budgetUsd`'s status: operator input,
|
|
5
|
+
// the agent may tighten and never widen, never self-raised. The agent has no time
|
|
6
|
+
// field in the plan vocabulary at all (addendum 2), so nothing here is reachable
|
|
7
|
+
// from an agent-authored document — this module is read by the runner only.
|
|
8
|
+
//
|
|
9
|
+
// Three measured facts shape it, none of them assumed:
|
|
10
|
+
//
|
|
11
|
+
// 1. `loop.stop()` CANNOT cut an in-flight call. Measured (F61/C1): fired at
|
|
12
|
+
// 500ms against a 4,000ms hanging generate(), run() returned at 4,018ms.
|
|
13
|
+
// bare-agent reads the flag at the round boundary (loop.js:661) and between
|
|
14
|
+
// tool calls (:916). So a wall deadline is a BETWEEN-ROUND check, and
|
|
15
|
+
// enforcement is `maxWallMs + closeStages × closeTimeoutMs` — reported, never
|
|
16
|
+
// rounded down to the requested number (F6 in a time coat). The multiplier is
|
|
17
|
+
// W5: a STAGED close (PRD v1.28) runs its stages one at a time and hands EACH
|
|
18
|
+
// one the full `closeTimeoutMs` (`runStages` → `runClose`, ralph.js:257), so a
|
|
19
|
+
// 4-stage close at 900s is 60 minutes of close, not 15. The runner's own
|
|
20
|
+
// outside watchdog already sized its stale window this way
|
|
21
|
+
// (scripts/run-u.mjs: `CLOSE_TIMEOUT_MS * spec.close.length`); the clock
|
|
22
|
+
// quoting the 1× number was the same arithmetic disagreeing with itself.
|
|
23
|
+
// That figure counts ONE close, and the RUNNER is what holds both of its loops
|
|
24
|
+
// to one: past the deadline it refuses to open a new fix iteration and stops on
|
|
25
|
+
// the verdict the last close already minted (W-2), and it refuses to open a
|
|
26
|
+
// STEP's attempt on the same reading — a step that would begin past the deadline
|
|
27
|
+
// is never funded. Before that second refusal a step starting with zero time left
|
|
28
|
+
// burned every attempt it was allowed (measured on the real flow: three attempts,
|
|
29
|
+
// three judged exits, all past the deadline), because the variance meter scores a
|
|
30
|
+
// step's time share against what the run had left when the step BEGAN and so is
|
|
31
|
+
// structurally blind to a step that began with nothing.
|
|
32
|
+
// It is still not a universal bound and is not claimed as one. Two overshoots
|
|
33
|
+
// remain outside this number, both deliberate. A step ALREADY UNDER WAY when the
|
|
34
|
+
// deadline trips finishes its attempt and runs its own exit checks (themselves
|
|
35
|
+
// close stages, at the full timeout each): grading real work always completes, or
|
|
36
|
+
// the run is unreadable after the money is spent. And that same mid-step deadline
|
|
37
|
+
// still routes through the ONE replan — the meter's own terminal, unchanged — so
|
|
38
|
+
// it pays a drafting call (floored at MIN_CALL_TIMEOUT_MS) before the new plan's
|
|
39
|
+
// first step is the one refused.
|
|
40
|
+
// 2. BA-18's provider timeout is the only instrument that fires on the ABSENCE
|
|
41
|
+
// of events. Measured (F61/C2): 1,259ms against a socket that accepts and
|
|
42
|
+
// never answers; without it the bound is the OS TCP timeout (~2h). Deriving
|
|
43
|
+
// it from the remaining budget is what stops the LAST round outliving the run.
|
|
44
|
+
// 3. ~90% accuracy is the bar, by hamr's ruling (addendum 3). This module does
|
|
45
|
+
// not chase precision; it reports honestly at the precision it has.
|
|
46
|
+
//
|
|
47
|
+
// Unbounded is a REAL state, not a large number: hamr ruled `maxWallMs` has no
|
|
48
|
+
// default, so a run without one is time-unbounded by explicit operator choice and
|
|
49
|
+
// says so (F45 — a defaulted cap is a silent second ceiling).
|
|
50
|
+
|
|
51
|
+
/** BA-18's default request/idle timeout (`AnthropicProvider.timeoutMs`), the
|
|
52
|
+
* ceiling every derived per-call timeout starts from. Kept in sync deliberately:
|
|
53
|
+
* a value ABOVE the provider's own default would be inert. */
|
|
54
|
+
export const PROVIDER_TIMEOUT_MS = 600_000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The floor for a derived per-call timeout. Below this a call cannot plausibly
|
|
58
|
+
* succeed, and a timeout that is certain to trip manufactures a `provider-red`
|
|
59
|
+
* out of a time budget — a transport CASUALTY, which is never evidence
|
|
60
|
+
* (F45/F48). The deadline check is what must stop a run out of time; the per-call
|
|
61
|
+
* timeout exists to bound a HANG, not to enforce the budget's last second.
|
|
62
|
+
*/
|
|
63
|
+
export const MIN_CALL_TIMEOUT_MS = 30_000;
|
|
64
|
+
|
|
65
|
+
/** bare-agent's rejection code when its request/idle timeout trips (BA-18,
|
|
66
|
+
* `provider-http.js` → `TimeoutError`). The error carries a `code` and NO
|
|
67
|
+
* `category`, so every consumer that defaults an uncategorised throw would file
|
|
68
|
+
* it as transport. */
|
|
69
|
+
export const TIMEOUT_CODE = 'ETIMEDOUT';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* F64 — is this throw the run's OWN deadline coming back dressed as a provider
|
|
73
|
+
* error? `callTimeoutMs()` hands the provider a bound derived from the wall, so
|
|
74
|
+
* when the wall is what shrank that bound, the resulting `TimeoutError` is a
|
|
75
|
+
* GOVERNANCE stop, not a casualty. Filing it as `provider-red` would discard it
|
|
76
|
+
* (a casualty is never evidence, F45/F48) and attach the F44
|
|
77
|
+
* `spendComplete:false` floor to a run that simply ran out of time.
|
|
78
|
+
*
|
|
79
|
+
* The discriminator is the wall itself, not the derived number: on a bounded
|
|
80
|
+
* clock a trip can only precede expiry when the bound came from the provider's
|
|
81
|
+
* own default (`remainingMs() > PROVIDER_TIMEOUT_MS`), and that case is transport
|
|
82
|
+
* by construction. So "already expired" IS the wall-derived set. An unbounded run
|
|
83
|
+
* can never produce one, and needs no separate `bounded` term to say so —
|
|
84
|
+
* `expired()` is false by construction with no cap. (A mutation run proved the
|
|
85
|
+
* extra term inert; the unbounded case is guarded by its own test instead, which
|
|
86
|
+
* is where that invariant belongs.)
|
|
87
|
+
*
|
|
88
|
+
* A genuine dead socket on a run whose wall has ALSO passed reads as a wall-halt.
|
|
89
|
+
* That is deliberate and honest: the run is out of time either way, the remedy is
|
|
90
|
+
* the same (raise `maxWallMs`), and the alternative — calling a governance stop
|
|
91
|
+
* transport on the chance that it was — is the failure this closes.
|
|
92
|
+
*
|
|
93
|
+
* @param {any} err the throw as caught
|
|
94
|
+
* @param {Clock} clock the run's clock
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function isWallTimeout(err, clock) {
|
|
98
|
+
return !!err && err.code === TIMEOUT_CODE && clock.expired();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} Clock
|
|
103
|
+
* @property {boolean} bounded whether the operator set a cap at all
|
|
104
|
+
* @property {number|null} requestedMs the operator's number; `null` when unbounded
|
|
105
|
+
* @property {number} closeStages how many close stages one verdict can pay for (W5)
|
|
106
|
+
* @property {() => number} elapsedMs wall time since the clock was created
|
|
107
|
+
* @property {() => number} remainingMs what is left, floored at 0; `Infinity` when unbounded
|
|
108
|
+
* @property {() => boolean} expired whether the deadline has passed (always false when unbounded)
|
|
109
|
+
* @property {(closeTimeoutMs: number) => number|null} enforcedMs the HONEST worst case
|
|
110
|
+
* @property {(providerDefaultMs?: number) => number} callTimeoutMs the derived per-call bound
|
|
111
|
+
* @property {(closeTimeoutMs: number) => object} report one spine-safe shape carrying both numbers
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Start a wall clock for one run.
|
|
116
|
+
* @param {{ maxWallMs?: number|null, closeStages?: number, now?: () => number }} [opts]
|
|
117
|
+
* `maxWallMs`: the signed operator cap, or absent/null for an honestly unbounded
|
|
118
|
+
* run. `closeStages`: how many stages the job's close runs (W5) — each one gets
|
|
119
|
+
* the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
|
|
120
|
+
* decoration. Defaults to 1, the single-predicate close. `now`: injected for
|
|
121
|
+
* tests — the default is the real clock.
|
|
122
|
+
* @returns {Clock}
|
|
123
|
+
*/
|
|
124
|
+
export function createClock({ maxWallMs = null, closeStages = 1, now = () => Date.now() } = {}) {
|
|
125
|
+
const startedAt = now();
|
|
126
|
+
// A cap is only a cap if it is a usable positive number. validateJob already
|
|
127
|
+
// reds anything else, so this is the belt (a caller that hand-builds a spec
|
|
128
|
+
// must not be able to turn a garbage value into a silent unbounded run OR a
|
|
129
|
+
// silent instant expiry).
|
|
130
|
+
const bounded = typeof maxWallMs === 'number' && Number.isFinite(maxWallMs) && maxWallMs > 0;
|
|
131
|
+
const cap = bounded ? maxWallMs : null;
|
|
132
|
+
// The same belt on the stage count. A close always runs at least once, so 1 is
|
|
133
|
+
// the floor: a 0 or a NaN sneaking in here would quote an enforced number BELOW
|
|
134
|
+
// the requested cap, which is the dishonesty this multiplier exists to remove.
|
|
135
|
+
const stages = typeof closeStages === 'number' && Number.isFinite(closeStages) && closeStages >= 1
|
|
136
|
+
? Math.floor(closeStages) : 1;
|
|
137
|
+
/** the ONE arithmetic, so the advertised record and the enforced worst case
|
|
138
|
+
* cannot be computed two ways (the two-transforms class, F9). */
|
|
139
|
+
const enforced = (/** @type {number} */ closeTimeoutMs) => (cap === null ? null : cap + stages * closeTimeoutMs);
|
|
140
|
+
|
|
141
|
+
const elapsedMs = () => now() - startedAt;
|
|
142
|
+
const remainingMs = () => (cap === null ? Infinity : Math.max(0, cap - elapsedMs()));
|
|
143
|
+
// Inclusive: AT the deadline is out of budget. A run resumes to cap, and the
|
|
144
|
+
// stop IS the checkpoint (v1.12) — so stopping one round early is correct and
|
|
145
|
+
// stopping one round late is the overshoot addendum 1 quantifies.
|
|
146
|
+
const expired = () => cap !== null && elapsedMs() >= cap;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
bounded,
|
|
150
|
+
requestedMs: cap,
|
|
151
|
+
closeStages: stages,
|
|
152
|
+
elapsedMs,
|
|
153
|
+
remainingMs,
|
|
154
|
+
expired,
|
|
155
|
+
enforcedMs: enforced,
|
|
156
|
+
callTimeoutMs: (providerDefaultMs = PROVIDER_TIMEOUT_MS) => {
|
|
157
|
+
if (cap === null) return providerDefaultMs;
|
|
158
|
+
// Clamped BOTH ways. Upper: never above the provider's own default, which
|
|
159
|
+
// would be inert. Lower: never below MIN_CALL_TIMEOUT_MS, and in
|
|
160
|
+
// particular never 0 — `0` DISABLES the provider timeout entirely
|
|
161
|
+
// (provider-anthropic.js), which is the exact opposite of the intent and
|
|
162
|
+
// would restore the ~2h OS-TCP hang on the last round of a tight budget.
|
|
163
|
+
return Math.min(providerDefaultMs, Math.max(MIN_CALL_TIMEOUT_MS, remainingMs()));
|
|
164
|
+
},
|
|
165
|
+
report: (closeTimeoutMs) => ({
|
|
166
|
+
bounded,
|
|
167
|
+
requestedMs: cap,
|
|
168
|
+
// On the record, not just in the arithmetic: without it a reader seeing
|
|
169
|
+
// enforced 90min against a requested 30min cannot tell a 60-minute close
|
|
170
|
+
// timeout from four 15-minute stages, and an unexplained gap is exactly the
|
|
171
|
+
// kind of number a later reader rounds back down to the requested one.
|
|
172
|
+
closeStages: stages,
|
|
173
|
+
enforcedMs: enforced(closeTimeoutMs),
|
|
174
|
+
elapsedMs: elapsedMs(),
|
|
175
|
+
// null, not Infinity: JSON.stringify turns Infinity into null anyway, and
|
|
176
|
+
// an append-only record must not depend on that coincidence — worse, a
|
|
177
|
+
// downstream `?? 0` on an Infinity would read as "no time left" (F6 class).
|
|
178
|
+
remainingMs: cap === null ? null : remainingMs(),
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/exits.js
CHANGED
|
@@ -9,8 +9,14 @@
|
|
|
9
9
|
// F45) and never gate intent (the F43 identical-refire trap: an allowed
|
|
10
10
|
// write that lands the same bytes is NOT a change).
|
|
11
11
|
// - a failing exit's detail is a MECHANICAL gap (a named wall, a count) —
|
|
12
|
-
// the genre that converts (F38/F46)
|
|
13
|
-
//
|
|
12
|
+
// the genre that converts (F38/F46). One detail is NOT names-and-counts:
|
|
13
|
+
// `json-valid` embeds `JSON.parse`'s own message and V8 quotes a window of
|
|
14
|
+
// the SOURCE inside it, so it can carry file bytes the worker chose. The
|
|
15
|
+
// CALLER scrubs, not this module: `runPlan` redacts every detail (the ONE
|
|
16
|
+
// `SECRET_PATTERNS` inventory) at the emission boundary, before anything
|
|
17
|
+
// rides the spine, which is append-only forever. `evalExits` is a public
|
|
18
|
+
// export — a direct caller wiring results into its own log applies its own
|
|
19
|
+
// redaction.
|
|
14
20
|
// - `check-passes` delegates through the `runCheck` seam the runner wires
|
|
15
21
|
// to the full runClose machinery; the evaluator itself never spawns. An
|
|
16
22
|
// unwired or throwing check fails CLOSED (the broken-close class: an
|
package/src/index.js
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
|
-
// bareloop public surface — grows one rung at a time (PRD §10).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// of human-signs-always (the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
1
|
+
// bareloop public surface — grows one rung at a time (PRD §10). The modules
|
|
2
|
+
// compose bottom-up: makeSpine feeds ralph, ralph is the wheel every layer is
|
|
3
|
+
// made of, and runJob is the one entry.
|
|
4
|
+
// The operator-owned job spec (job-v1) is the arbiter's rulebook — validateJob
|
|
5
|
+
// and validatePlan are siblings, never extensions of each other; jobSpecHash /
|
|
6
|
+
// checkApproval are the pure half of human-signs-always (the runner enforces).
|
|
7
|
+
// runJob: approval gate → primitive smoke → the plan flow under the ONE ledger
|
|
8
|
+
// (unpriced is never free, F6). The upstream ledger folds spines into one
|
|
9
|
+
// append-only incident file; filing stays human (suggestedAsk is a seed, never
|
|
10
|
+
// an auto-file).
|
|
11
|
+
//
|
|
12
|
+
// The config-v1 exports (`validateConfig`, `interpret`, `extractRules`,
|
|
13
|
+
// `diffPaths`, `LOOP_SHAPES`, `SLOTS`, `VERBS`, `STALL_REDS`, `CLASSES`,
|
|
14
|
+
// `STEP_MODES`) were REMOVED 2026-07-26 with the operator-authored steps[] path
|
|
15
|
+
// (PRD v1.32) — a breaking change, and the reason for the version bump.
|
|
12
16
|
|
|
13
17
|
export { makeSpine } from './spine.js';
|
|
14
18
|
// CLOSE_FAULTS/scanSecrets: bareloop.context.md documents both as public API —
|
|
15
19
|
// the exports make the adopter contract true (release review 2026-07-19; the
|
|
16
20
|
// exports map admits only ".", so a deep import cannot reach them).
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// runStages/checkMenu/STORE_VERBS: same reason, one rung later (review 2026-07-30)
|
|
22
|
+
// — the CHANGELOG announces runStages async beside runClose and STORE_VERBS beside
|
|
23
|
+
// the exported WRITE_VERBS, and bareloop.context.md points adopters at checkMenu by
|
|
24
|
+
// name. Documented-but-unexported is a false contract; the additions are additive.
|
|
25
|
+
export { ralph, runClose, runStages, CLOSE_FAULTS } from './ralph.js';
|
|
26
|
+
export { globToPrefix, scanSecrets } from './validate.js';
|
|
27
|
+
export { validateJob, jobSpecHash, checkApproval, checkMenu, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from './job.js';
|
|
20
28
|
// Layer 2: the plan-v1 validator — the agent-authored plan doc's gate; the
|
|
21
29
|
// two-doc split's third validator never happens (plan-v1 gates the PLAN, the
|
|
22
30
|
// job spec stays the arbiter's only home).
|
|
23
|
-
export { validatePlan, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from './plan.js';
|
|
31
|
+
export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from './plan.js';
|
|
24
32
|
export { snapshotScope, evalExits } from './exits.js';
|
|
25
33
|
export { runPlan } from './planrun.js';
|
|
26
|
-
export { interpret, STALL_REDS } from './interpret.js';
|
|
27
|
-
export { extractRules, MAX_RULES, MAX_RULE_CHARS } from './extract.js';
|
|
28
34
|
export { runJob } from './run.js';
|
|
29
35
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from './ledger.js';
|