bareloop 0.6.0 → 0.8.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 +690 -0
- package/README.md +30 -11
- package/bareloop.context.md +658 -38
- package/package.json +2 -2
- package/src/bridges.js +571 -0
- package/src/clock.js +57 -5
- package/src/exits.js +8 -2
- package/src/index.js +29 -0
- package/src/job.js +43 -6
- package/src/ladder.js +196 -0
- package/src/ledger.js +84 -9
- package/src/plan.js +106 -107
- package/src/planrun.js +1040 -168
- package/src/ralph.js +156 -29
- package/src/reuse.js +1665 -0
- package/src/run.js +92 -6
- package/src/selection.js +160 -0
- package/src/spine.js +7 -2
- package/src/trend.js +405 -0
- package/src/validate.js +121 -0
- package/types/bridges.d.ts +198 -0
- package/types/clock.d.ts +25 -3
- package/types/exits.d.ts +14 -0
- package/types/index.d.ts +3 -0
- package/types/job.d.ts +13 -2
- package/types/ladder.d.ts +86 -0
- package/types/plan.d.ts +41 -41
- package/types/planrun.d.ts +208 -9
- package/types/ralph.d.ts +98 -4
- package/types/reuse.d.ts +392 -0
- package/types/run.d.ts +63 -3
- package/types/selection.d.ts +46 -0
- package/types/spine.d.ts +8 -1
- package/types/trend.d.ts +108 -0
- package/types/validate.d.ts +50 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D6's status ladder, walked in history order. Pure, total, and never throws —
|
|
3
|
+
* it is called on whatever a file happens to contain.
|
|
4
|
+
*
|
|
5
|
+
* - **candidate** — at least one green.
|
|
6
|
+
* - **proven** — greens on ≥2 DISTINCT patients (R2: the same SHAPE on a second
|
|
7
|
+
* instance; two greens on ONE patient is a lookup table, which the memorization
|
|
8
|
+
* audit kills).
|
|
9
|
+
* - a **red** on a PROVEN entry drops it to candidate, and re-promotion then needs
|
|
10
|
+
* greens on two distinct patients AGAIN — that is what "until a new green on a
|
|
11
|
+
* second distinct patient re-proves" means, so the demoting red clears the
|
|
12
|
+
* distinct-patient set rather than leaving the old count standing to re-promote
|
|
13
|
+
* the entry on the first repeat green.
|
|
14
|
+
* - a red on a CANDIDATE changes nothing: there is nothing below the entry bar, and
|
|
15
|
+
* the entry stays in the record with the red beside it.
|
|
16
|
+
* - a **CASUALTY is not a red.** Only the literal outcome `'red'` demotes; every
|
|
17
|
+
* other non-green outcome (`provider-red`, `wall-halt`, `close-crashed`, …) is a
|
|
18
|
+
* casualty, and casualties are never evidence in either direction.
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} history the bridge's per-run rows, oldest first
|
|
21
|
+
* @returns {'candidate'|'proven'|null} null = below the entry bar (no green yet)
|
|
22
|
+
*/
|
|
23
|
+
export function deriveStatus(history: unknown): "candidate" | "proven" | null;
|
|
24
|
+
/**
|
|
25
|
+
* Validate a bridge entry (`schema: "bridge-v1"`). Never throws on JSON text or
|
|
26
|
+
* plain parsed data — the same ingest contract both config validators hold; every
|
|
27
|
+
* failure is a named red. Returns the parsed entry on ok, null on any red.
|
|
28
|
+
* @param {object|string|unknown} input parsed entry, or raw JSON text
|
|
29
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
30
|
+
*/
|
|
31
|
+
export function validateBridge(input: object | string | unknown): {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
reds: Red[];
|
|
34
|
+
bridge: any;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* FIRST GREEN MINTS: build a new registry entry from one green. The entry bar is
|
|
38
|
+
* a green (D6) — there is deliberately no way to create an entry without one, so
|
|
39
|
+
* a failed plan cannot enter the registry even by accident.
|
|
40
|
+
* @param {unknown} meta `{ name, goal, specHash, closeStageNames, toolsUsed }` —
|
|
41
|
+
* `specHash` may be an explicit null (one probe-era bridge on disk has none), and
|
|
42
|
+
* the KEY is required so an unknown hash has to be said rather than omitted.
|
|
43
|
+
* @param {unknown} record `{ runid, patient, at, plan, costUsd, spendComplete, wallMs, rounds, specHash? }`
|
|
44
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
45
|
+
*/
|
|
46
|
+
export function mintBridge(meta: unknown, record: unknown): {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
reds: Red[];
|
|
49
|
+
bridge: any;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A green on an EXISTING bridge: the plan AS EXECUTED becomes its next version,
|
|
53
|
+
* and the run gets a history row. Pure — returns a NEW entry; the input is never
|
|
54
|
+
* mutated, so a caller that reds still holds the bridge it read from disk.
|
|
55
|
+
* @param {unknown} bridge a validated bridge entry
|
|
56
|
+
* @param {unknown} record `{ runid, patient, at, plan, costUsd, spendComplete, wallMs, rounds, specHash? }`
|
|
57
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
58
|
+
*/
|
|
59
|
+
export function appendGreen(bridge: unknown, record: unknown): {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
reds: Red[];
|
|
62
|
+
bridge: any;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A red (or a casualty): a history row ONLY. R1 — the bridge file is never edited
|
|
66
|
+
* in place by a red; the recipe that greened stays exactly as it greened, and the
|
|
67
|
+
* status derivation reads the demotion off the history.
|
|
68
|
+
* @param {unknown} bridge a validated bridge entry
|
|
69
|
+
* @param {unknown} record `{ runid, patient, at, outcome, failingStage, costUsd, spendComplete, wallMs, rounds }`
|
|
70
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
71
|
+
*/
|
|
72
|
+
export function appendRed(bridge: unknown, record: unknown): {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
reds: Red[];
|
|
75
|
+
bridge: any;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* One row of D3's listing: what the user (and the selecting LLM) sees per bridge —
|
|
79
|
+
* name, the job sentence it greened, status, greens/reds, and the cost/time BAND of
|
|
80
|
+
* its greens. Deliberately no score (D6).
|
|
81
|
+
*
|
|
82
|
+
* The cost band SKIPS unknowns and says how many it skipped: an aggregate that
|
|
83
|
+
* treats a null as 0 reports a floor that reads as exact, which is F6 in an honest
|
|
84
|
+
* coat. With no priced green at all, every bound is null — never 0.
|
|
85
|
+
*
|
|
86
|
+
* Total and never throws: the listing is a display surface, and one malformed
|
|
87
|
+
* entry must not take the whole listing down (loadRegistry already reports it).
|
|
88
|
+
* @param {unknown} bridge
|
|
89
|
+
* @returns {{name: string|null, goal: string|null, status: 'candidate'|'proven'|null, greens: number, reds: number, lastOutcome: string|null, greenCost: {minUsd: number|null, maxUsd: number|null, minWallMs: number|null, maxWallMs: number|null, unpricedCount: number, untimedCount: number}}}
|
|
90
|
+
*/
|
|
91
|
+
export function listingRow(bridge: unknown): {
|
|
92
|
+
name: string | null;
|
|
93
|
+
goal: string | null;
|
|
94
|
+
status: "candidate" | "proven" | null;
|
|
95
|
+
greens: number;
|
|
96
|
+
reds: number;
|
|
97
|
+
lastOutcome: string | null;
|
|
98
|
+
greenCost: {
|
|
99
|
+
minUsd: number | null;
|
|
100
|
+
maxUsd: number | null;
|
|
101
|
+
minWallMs: number | null;
|
|
102
|
+
maxWallMs: number | null;
|
|
103
|
+
unpricedCount: number;
|
|
104
|
+
untimedCount: number;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* D2's LOAD-TIME gate, as SPLIT by the 2026-08-01 addendum. Exactly three checks —
|
|
109
|
+
* *"is this the right KIND of recipe?"*, asked at the box:
|
|
110
|
+
*
|
|
111
|
+
* 1. **verdict type matches.** v1 admits `green` only, and a bridge only ever
|
|
112
|
+
* exists because a green minted it, so the bridge side is `green` by
|
|
113
|
+
* construction and the check reads the JOB's declared type. **The day a second
|
|
114
|
+
* verdict class is admitted, the bridge entry must carry its own verdictType
|
|
115
|
+
* and this becomes a two-sided comparison** — recorded here so it cannot be
|
|
116
|
+
* quietly assumed to already be one.
|
|
117
|
+
* 2. **close-stage KINDS match.** v1 simplification: the same stage NAMES in the
|
|
118
|
+
* same ORDER is the definition of the same kinds. The probes' R2-legal pair
|
|
119
|
+
* matched on four identical stage names over different patients, which is the
|
|
120
|
+
* material this rule was ruled on; a real kind-comparison (what each stage
|
|
121
|
+
* INSPECTS, independent of what it was named) is a later rung's problem and is
|
|
122
|
+
* not pretended here.
|
|
123
|
+
* 3. **every stored verb fits THIS job's signed menu.** An omitted `tools` means
|
|
124
|
+
* the concrete current TOOL_MENU — the same predicate `resolveSpec` and
|
|
125
|
+
* `validatePlan` read (MED-1), so what the gate judges is what was signed.
|
|
126
|
+
*
|
|
127
|
+
* **NOTHING about paths, scopes or targets.** Those are instance-bound and are
|
|
128
|
+
* EXPECTED to red: every recipe from a different day names yesterday's bricks, and
|
|
129
|
+
* that is what a recipe IS, not a flaw. The pre-probe measured the drafter
|
|
130
|
+
* replacing them unaided (3/3 legal after tweak), while the gate as originally
|
|
131
|
+
* frozen would have refused all six of them at the door. The full `validatePlan`
|
|
132
|
+
* still judges the TWEAKED draft at draft time — nothing executes unvalidated, and
|
|
133
|
+
* no second, looser path exists for an inherited plan (D4).
|
|
134
|
+
*
|
|
135
|
+
* A failing gate is the **recipe-stale / wrong-kind** OUTCOME — a distinct result
|
|
136
|
+
* with cold drafting offered, never a throw.
|
|
137
|
+
* @param {unknown} bridge
|
|
138
|
+
* @param {unknown} job the validateJob-green spec this run is signed against
|
|
139
|
+
* @returns {{ ok: boolean, reds: Red[] }}
|
|
140
|
+
*/
|
|
141
|
+
export function loadGate(bridge: unknown, job: unknown): {
|
|
142
|
+
ok: boolean;
|
|
143
|
+
reds: Red[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Read one bridge file. Never throws — a missing/unreadable file and a malformed
|
|
147
|
+
* one are both named reds, because the registry is read at selection time and one
|
|
148
|
+
* bad file must not take the run down.
|
|
149
|
+
* @param {string} file
|
|
150
|
+
* @returns {{ ok: boolean, reds: Red[], bridge: any }}
|
|
151
|
+
*/
|
|
152
|
+
export function loadBridge(file: string): {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
reds: Red[];
|
|
155
|
+
bridge: any;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Read a whole registry directory. A malformed entry is SKIPPED AND REPORTED —
|
|
159
|
+
* one red per bad file, the good entries still returned, sorted by name for a
|
|
160
|
+
* stable listing. `ok` is false when anything was skipped, so a caller can surface
|
|
161
|
+
* the skip instead of silently listing fewer workflows than exist on disk.
|
|
162
|
+
* @param {string} dir the operator-supplied registry path (no default location)
|
|
163
|
+
* @returns {{ ok: boolean, reds: Red[], bridges: any[] }}
|
|
164
|
+
*/
|
|
165
|
+
export function loadRegistry(dir: string): {
|
|
166
|
+
ok: boolean;
|
|
167
|
+
reds: Red[];
|
|
168
|
+
bridges: any[];
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Write one bridge entry, ATOMICALLY (temp + rename): a registry read concurrent
|
|
172
|
+
* with a write must see the old entry or the new one, never a half-written file
|
|
173
|
+
* that then reads as a malformed skip. An INVALID entry is refused before anything
|
|
174
|
+
* touches the disk — the registry never holds a file its own validator rejects.
|
|
175
|
+
* @param {string} dir the operator-supplied registry directory (must exist)
|
|
176
|
+
* @param {unknown} bridge
|
|
177
|
+
* @returns {{ ok: boolean, reds: Red[], file: string|null }}
|
|
178
|
+
*/
|
|
179
|
+
export function saveBridge(dir: string, bridge: unknown): {
|
|
180
|
+
ok: boolean;
|
|
181
|
+
reds: Red[];
|
|
182
|
+
file: string | null;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Create a registry directory. Separate from `saveBridge` ON PURPOSE: saving must
|
|
186
|
+
* never conjure a registry from a typo'd path, so making one is an explicit act.
|
|
187
|
+
* @param {string} dir
|
|
188
|
+
* @returns {string} the directory
|
|
189
|
+
*/
|
|
190
|
+
export function makeRegistry(dir: string): string;
|
|
191
|
+
/** @param {string} dir @returns {boolean} does a registry directory exist */
|
|
192
|
+
export function registryExists(dir: string): boolean;
|
|
193
|
+
export const BRIDGE_SCHEMA: "bridge-v1";
|
|
194
|
+
export type Red = {
|
|
195
|
+
code: string;
|
|
196
|
+
path: string;
|
|
197
|
+
detail?: string;
|
|
198
|
+
};
|
package/types/clock.d.ts
CHANGED
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* the same (raise `maxWallMs`), and the alternative — calling a governance stop
|
|
21
21
|
* transport on the chance that it was — is the failure this closes.
|
|
22
22
|
*
|
|
23
|
+
* BOTH derived bounds land here, because both are derived from the same wall:
|
|
24
|
+
* BA-18's idle timeout (`callTimeoutMs`) and BA-19's total-duration deadline
|
|
25
|
+
* (`callDeadlineMs`). Adding the second CODE does not widen the rule — the
|
|
26
|
+
* discriminator is still `clock.expired()`, never the code. That matters for the
|
|
27
|
+
* deadline specifically: a provider someone else constructed with its own
|
|
28
|
+
* `deadlineMs` could throw this code on a run with time left, and that is transport,
|
|
29
|
+
* exactly as it was before.
|
|
30
|
+
*
|
|
23
31
|
* @param {any} err the throw as caught
|
|
24
32
|
* @param {Clock} clock the run's clock
|
|
25
33
|
* @returns {boolean}
|
|
@@ -35,22 +43,25 @@ export function isWallTimeout(err: any, clock: Clock): boolean;
|
|
|
35
43
|
* @property {() => boolean} expired whether the deadline has passed (always false when unbounded)
|
|
36
44
|
* @property {(closeTimeoutMs: number) => number|null} enforcedMs the HONEST worst case
|
|
37
45
|
* @property {(providerDefaultMs?: number) => number} callTimeoutMs the derived per-call bound
|
|
46
|
+
* @property {() => number|null} callDeadlineMs the derived per-call TOTAL-duration ceiling; `null` when unbounded
|
|
38
47
|
* @property {(closeTimeoutMs: number) => object} report one spine-safe shape carrying both numbers
|
|
39
48
|
*/
|
|
40
49
|
/**
|
|
41
50
|
* Start a wall clock for one run.
|
|
42
|
-
* @param {{ maxWallMs?: number|null, closeStages?: number, now?: () => number }} [opts]
|
|
51
|
+
* @param {{ maxWallMs?: number|null, closeStages?: number, now?: () => number, priorElapsedMs?: number }} [opts]
|
|
43
52
|
* `maxWallMs`: the signed operator cap, or absent/null for an honestly unbounded
|
|
44
53
|
* run. `closeStages`: how many stages the job's close runs (W5) — each one gets
|
|
45
54
|
* the FULL `closeTimeoutMs`, so this is the multiplier on the overshoot, not a
|
|
46
55
|
* decoration. Defaults to 1, the single-predicate close. `now`: injected for
|
|
47
|
-
* tests — the default is the real clock.
|
|
56
|
+
* tests — the default is the real clock. `priorElapsedMs`: time this attempt's
|
|
57
|
+
* PREDECESSOR already consumed (see below).
|
|
48
58
|
* @returns {Clock}
|
|
49
59
|
*/
|
|
50
|
-
export function createClock({ maxWallMs, closeStages, now }?: {
|
|
60
|
+
export function createClock({ maxWallMs, closeStages, now, priorElapsedMs }?: {
|
|
51
61
|
maxWallMs?: number | null;
|
|
52
62
|
closeStages?: number;
|
|
53
63
|
now?: () => number;
|
|
64
|
+
priorElapsedMs?: number;
|
|
54
65
|
}): Clock;
|
|
55
66
|
/** BA-18's default request/idle timeout (`AnthropicProvider.timeoutMs`), the
|
|
56
67
|
* ceiling every derived per-call timeout starts from. Kept in sync deliberately:
|
|
@@ -69,6 +80,13 @@ export const MIN_CALL_TIMEOUT_MS: 30000;
|
|
|
69
80
|
* `category`, so every consumer that defaults an uncategorised throw would file
|
|
70
81
|
* it as transport. */
|
|
71
82
|
export const TIMEOUT_CODE: "ETIMEDOUT";
|
|
83
|
+
/** bare-agent's rejection code when its TOTAL-duration deadline trips (BA-19,
|
|
84
|
+
* `provider-http.js` → `applyRequestDeadline`). A DIFFERENT code from the idle
|
|
85
|
+
* bound's on purpose — upstream splits them precisely so a consumer routing
|
|
86
|
+
* governance stops apart from transport casualties can tell which timer fired. Like
|
|
87
|
+
* `TIMEOUT_CODE` it carries no `category`, so every default-to-transport catch site
|
|
88
|
+
* would file it as a casualty. */
|
|
89
|
+
export const DEADLINE_CODE: "EDEADLINE";
|
|
72
90
|
export type Clock = {
|
|
73
91
|
/**
|
|
74
92
|
* whether the operator set a cap at all
|
|
@@ -102,6 +120,10 @@ export type Clock = {
|
|
|
102
120
|
* the derived per-call bound
|
|
103
121
|
*/
|
|
104
122
|
callTimeoutMs: (providerDefaultMs?: number) => number;
|
|
123
|
+
/**
|
|
124
|
+
* the derived per-call TOTAL-duration ceiling; `null` when unbounded
|
|
125
|
+
*/
|
|
126
|
+
callDeadlineMs: () => number | null;
|
|
105
127
|
/**
|
|
106
128
|
* one spine-safe shape carrying both numbers
|
|
107
129
|
*/
|
package/types/exits.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ export function evalExits(exits: Array<Record<string, any>>, { dir, snapshot, ru
|
|
|
36
36
|
pass: boolean;
|
|
37
37
|
results: ExitResult[];
|
|
38
38
|
}>;
|
|
39
|
+
/** The check gap is ALREADY bounded upstream by runClose's boundGap (head +
|
|
40
|
+
* gapKeep block ≤ 8192 + tail ≈ 10KB, with the failing-test NAMES preserved).
|
|
41
|
+
* The evaluator must NOT re-truncate it: a 400-char clamp deletes exactly those
|
|
42
|
+
* `not ok`/`FAILED` names — the mechanical gap the F46 conversion mechanism
|
|
43
|
+
* feeds the worker (F28 reintroduced). This cap is a defensive backstop above
|
|
44
|
+
* boundGap's own envelope: a normal bounded gap passes through intact, and only
|
|
45
|
+
* a pathological seam that returned an unbounded gap would ever be trimmed.
|
|
46
|
+
*
|
|
47
|
+
* Exported for the ONE reason `boundGap`/`GAP_KEEP_TRIM_MARKER` are (src/ralph.js):
|
|
48
|
+
* the replan brief re-bounds this same artifact one seam later (`closeGapBlock`), and
|
|
49
|
+
* a second hand-spelled ceiling there would be a second answer to "how big may a
|
|
50
|
+
* check gap be" — the two would drift on exactly the property that matters, whether
|
|
51
|
+
* the `not ok`/`FAILED` names survive. */
|
|
52
|
+
export const CHECK_GAP_MAX: 12000;
|
|
39
53
|
/**
|
|
40
54
|
* `fault` present means the INSTRUMENT could not judge (unwired snapshot/seam,
|
|
41
55
|
* a check in runClose's forbidden zone) — it carries the runClose verdict name
|
package/types/index.d.ts
CHANGED
|
@@ -6,4 +6,7 @@ export { globToPrefix, scanSecrets } from "./validate.js";
|
|
|
6
6
|
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";
|
|
7
7
|
export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
|
|
8
8
|
export { snapshotScope, evalExits } from "./exits.js";
|
|
9
|
+
export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists } from "./bridges.js";
|
|
10
|
+
export { renderListing, selectionPrompt } from "./selection.js";
|
|
11
|
+
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from "./reuse.js";
|
|
9
12
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
|
package/types/job.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red — `verb`
|
|
1
|
+
/** @typedef {{code: string, path: string, detail?: string, verb?: string, lib?: string}} Red — `verb` and `lib` ride request-reds as structured data (the ledger keys on them, never on prose).
|
|
2
|
+
* `lib` is the TERRITORY the demand lands against, stamped HERE at the emit site
|
|
3
|
+
* (the typed-lib rule): one code, two catalogues — a locked TOOL verb is demand
|
|
4
|
+
* against the worker-surface package, a locked VERDICT type is demand against
|
|
5
|
+
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
6
|
+
* filed as an upstream bug (the BA-2 misattribution class). */
|
|
2
7
|
/**
|
|
3
8
|
* Validate an operator-owned job spec (`schema: "job-v1"`). Never throws on
|
|
4
9
|
* JSON text or plain parsed data — the ingest contract; a live object with a
|
|
@@ -165,11 +170,17 @@ export const PROVIDERS: readonly string[];
|
|
|
165
170
|
* not duplicated here). */
|
|
166
171
|
export const CONDITION_KEYS: readonly string[];
|
|
167
172
|
/**
|
|
168
|
-
* — `verb`
|
|
173
|
+
* — `verb` and `lib` ride request-reds as structured data (the ledger keys on them, never on prose).
|
|
174
|
+
* `lib` is the TERRITORY the demand lands against, stamped HERE at the emit site
|
|
175
|
+
* (the typed-lib rule): one code, two catalogues — a locked TOOL verb is demand
|
|
176
|
+
* against the worker-surface package, a locked VERDICT type is demand against
|
|
177
|
+
* bareloop's OWN menu. Inferring it downstream is how a bareloop refusal gets
|
|
178
|
+
* filed as an upstream bug (the BA-2 misattribution class).
|
|
169
179
|
*/
|
|
170
180
|
export type Red = {
|
|
171
181
|
code: string;
|
|
172
182
|
path: string;
|
|
173
183
|
detail?: string;
|
|
174
184
|
verb?: string;
|
|
185
|
+
lib?: string;
|
|
175
186
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a gap for comparison. Deliberately NARROW: only bytes a re-run changes
|
|
3
|
+
* on its own are removed. Error COUNTS, file names and line numbers all survive —
|
|
4
|
+
* a trajectory like "30 errors" → "22 errors" must read as two different gaps, and
|
|
5
|
+
* a normalizer that scrubbed numbers would turn the converging case this module
|
|
6
|
+
* exists to protect into an instant strike.
|
|
7
|
+
* @param {string} text @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeGap(text: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* The comparable key for a gap: the normalized text, hashed. Hashed rather than
|
|
12
|
+
* kept whole because the seen-set lives for the whole step and a close's output
|
|
13
|
+
* can be tens of KB — and because the set holds no content, nothing here can leak
|
|
14
|
+
* model or file bytes into a record.
|
|
15
|
+
* @param {string} text @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function gapKey(text: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Build one ladder. ONE PER STEP: the seen-set and the strike count are the step's
|
|
20
|
+
* own history, and a ladder shared across steps would strike a fresh step for a gap
|
|
21
|
+
* an earlier one had already seen.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {number} [opts.limit] strikes that end the step (default `STRIKE_LIMIT`)
|
|
25
|
+
* @param {() => number} opts.writeCount the CUMULATIVE count of the worker's
|
|
26
|
+
* allow-decision write/edit records so far this step (planrun reads it off the
|
|
27
|
+
* gate audit). Required, and a throw when it is missing rather than a silent
|
|
28
|
+
* degradation to repeats-only: a strike rule running on one of its two signals
|
|
29
|
+
* with nothing on the record saying so is the blind-instrument class, and this
|
|
30
|
+
* is the BA-4 param-guard class where a throw is the right answer.
|
|
31
|
+
*/
|
|
32
|
+
export function createLadder({ limit, writeCount }: {
|
|
33
|
+
limit?: number | undefined;
|
|
34
|
+
writeCount: () => number;
|
|
35
|
+
}): {
|
|
36
|
+
/**
|
|
37
|
+
* Read one RED iteration. Called by ralph after the judge has rendered a
|
|
38
|
+
* non-green, non-fault verdict — a green never reaches here, so every reading
|
|
39
|
+
* is a failing iteration by construction.
|
|
40
|
+
* @param {{iteration: number, gap?: string}} o
|
|
41
|
+
* @returns {{governor: string, iteration: number, strike: boolean, strikes: number,
|
|
42
|
+
* limit: number, wrote: boolean, repeatOf: number|null, distinctGaps: number}}
|
|
43
|
+
*/
|
|
44
|
+
record({ iteration, gap }: {
|
|
45
|
+
iteration: number;
|
|
46
|
+
gap?: string;
|
|
47
|
+
}): {
|
|
48
|
+
governor: string;
|
|
49
|
+
iteration: number;
|
|
50
|
+
strike: boolean;
|
|
51
|
+
strikes: number;
|
|
52
|
+
limit: number;
|
|
53
|
+
wrote: boolean;
|
|
54
|
+
repeatOf: number | null;
|
|
55
|
+
distinctGaps: number;
|
|
56
|
+
};
|
|
57
|
+
/** has the step run out of strikes? */
|
|
58
|
+
struckOut: () => boolean;
|
|
59
|
+
/** the ladder's own books — counts and iteration numbers, never gap content */
|
|
60
|
+
report: () => {
|
|
61
|
+
iterations: number;
|
|
62
|
+
strikes: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
distinctGaps: number;
|
|
65
|
+
repeats: {
|
|
66
|
+
at: number;
|
|
67
|
+
firstAt: number;
|
|
68
|
+
}[];
|
|
69
|
+
idle: number[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The mechanism, in one sentence, for the replan brief. It is the only channel
|
|
73
|
+
* the redrafting planner adapts to, so it states WHICH shape ended the step and
|
|
74
|
+
* the evidence for it — never a label on its own, and never a culprit file
|
|
75
|
+
* beyond what the close itself reported (F28): the inputs here are iteration
|
|
76
|
+
* numbers and counts, so naming one is inexpressible.
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
brief(): string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The strike ceiling. Shell-owned, exactly where the fixed count was: the runner
|
|
83
|
+
* sets it, the agent cannot express it, and it is arbiter territory — a step's own
|
|
84
|
+
* plan may not tighten it and may not raise it. Two is hamr's number.
|
|
85
|
+
*/
|
|
86
|
+
export const STRIKE_LIMIT: 2;
|
package/types/plan.d.ts
CHANGED
|
@@ -24,42 +24,6 @@
|
|
|
24
24
|
* @returns {string[]} the offerable scopes, deduped, shallowest-first, capped
|
|
25
25
|
*/
|
|
26
26
|
export function legalScopes(writeScope: string[], dirs?: string[], cap?: number): string[];
|
|
27
|
-
/**
|
|
28
|
-
* F49 — a heuristic reject for the catastrophic-backtracking footgun: an
|
|
29
|
-
* unbounded quantifier applied to a group whose body itself repeats unboundedly
|
|
30
|
-
* (`(a+)+`, `(\d*)*`, `([a-z]+){1,}`) — including through a redundant wrapping
|
|
31
|
-
* group (`((a+))+`, `(?:(a+))*`), which is the SAME exponential class as `(a+)+`
|
|
32
|
-
* and is caught by propagating the inner repeat up through the wrapper. Such a
|
|
33
|
-
* pattern can hang `RegExp.test` for
|
|
34
|
-
* seconds on a short crafted body, and `evalExits` runs the agent-authored
|
|
35
|
-
* `artifact-written` pattern with NO timeout. This is self-DoS only — the agent
|
|
36
|
-
* authors both the pattern and (via the worker) the artifact, so a hang burns
|
|
37
|
-
* only its own run's wall-clock; there is NO arbiter compromise (it cannot
|
|
38
|
-
* escape the fence, forge a green, or leak a secret). So the reject targets the
|
|
39
|
-
* dominant exponential class and fails it as a mechanical gap at the validation
|
|
40
|
-
* gate, before any tokens burn. A full ReDoS analyzer needs a real regex engine
|
|
41
|
-
* (an external native dep we do not take for a LOW issue); exotic
|
|
42
|
-
* overlapping-alternation blowup is out of scope by decision.
|
|
43
|
-
*
|
|
44
|
-
* SCOPE IS ASYMMETRIC, both directions named on purpose:
|
|
45
|
-
* - false NEGATIVE: overlapping-alternation blowup (`(a|ab)+`-class) is not
|
|
46
|
-
* detected — out of scope (self-DoS only, no arbiter compromise).
|
|
47
|
-
* - false POSITIVE: a group whose repetitions are disambiguated by a literal
|
|
48
|
-
* anchor/delimiter (`(?:^- .+$\n?)+`, `(?:CHANGELOG:.+\n)+`) is FLAGGED even
|
|
49
|
-
* though a real engine runs it linearly — the scan sees the nested-quantifier
|
|
50
|
-
* SHAPE, not the disambiguation. Rejecting it is the FAIL-SAFE direction (it
|
|
51
|
-
* never admits an unsafe pattern), and the cost is bounded: the plan drafter
|
|
52
|
-
* gets a mechanical gap and rewrites (drop the outer `+`, or match once).
|
|
53
|
-
* Detecting "safe because anchored" needs the same real engine we declined,
|
|
54
|
-
* and guessing it wrong would ADMIT an exponential pattern — so the shape
|
|
55
|
-
* reject stands, and the over-rejection is a named, accepted limitation.
|
|
56
|
-
*
|
|
57
|
-
* The input is guaranteed to compile as a RegExp (checked first by the caller),
|
|
58
|
-
* so this scan assumes valid, balanced JS regex syntax.
|
|
59
|
-
* @param {string} src a compiled regex source string
|
|
60
|
-
* @returns {boolean} true iff a nested unbounded quantifier is present
|
|
61
|
-
*/
|
|
62
|
-
export function hasNestedQuantifier(src: string): boolean;
|
|
63
27
|
/**
|
|
64
28
|
* The close, as the ordered STAGE LIST the plan flow actually executes (PRD
|
|
65
29
|
* v1.28). A staged close is already that list; the legacy object form is the
|
|
@@ -90,7 +54,7 @@ export function stageClose(close: any): any[] | null;
|
|
|
90
54
|
* job spec. Never throws on JSON text or plain parsed data; every failure is
|
|
91
55
|
* a named red. Returns the parsed plan on ok (single parse), null on any red.
|
|
92
56
|
* @param {object|string} input parsed plan, or raw JSON text (parse failures are a red)
|
|
93
|
-
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[],
|
|
57
|
+
* @param {{ job?: any, maxStepRounds?: number, scopes?: string[], seedRed?: string[], priorChecks?: string[] }} [opts] `job`: the
|
|
94
58
|
* validateJob-GREEN four-field spec (the ceiling, the fence, and the checks
|
|
95
59
|
* menu all come from it — a missing or non-plan-shape job fails CLOSED);
|
|
96
60
|
* `maxStepRounds`: the shell's per-step rounds ceiling (interpret's
|
|
@@ -98,13 +62,28 @@ export function stageClose(close: any): any[] | null;
|
|
|
98
62
|
* `scopes`: the offered `tree-changed` menu from `legalScopes` (the SAME array
|
|
99
63
|
* the drafting prompt enumerated). Omitted, it derives from the signed
|
|
100
64
|
* writeScope — never a free-text fallback (F50).
|
|
65
|
+
*
|
|
66
|
+
* The two shape-lottery gate rules (2026-08-04) — both keyed on RECORDED
|
|
67
|
+
* facts, never an LLM judgment of "is this job small", and both inactive
|
|
68
|
+
* when their fact is omitted (resume/reuse callers stay byte-identical):
|
|
69
|
+
* `seedRed`: check names whose PREFLIGHT verdict was needs_revision. Rule
|
|
70
|
+
* A-v2 — a seed-red check is the goal itself, not a milestone; it may only
|
|
71
|
+
* sit on the plan's FINAL write step (`check-placement`). The $0 archive
|
|
72
|
+
* sweep: seed-red-check-on-an-early-step has 0 honest greens ever; the one
|
|
73
|
+
* wide closing step greens 7/7. Green-at-seed checks stay free mid-plan
|
|
74
|
+
* (the 20 TESTGEN mid-plan greens — a guard, not a goal).
|
|
75
|
+
* `priorChecks`: the check names the PREDECESSOR plan carried (replan gate
|
|
76
|
+
* only). Rule B — a redraft may not drop one (`check-shed`): exits without
|
|
77
|
+
* the check verify FORM alone, so a shed lets the run "green" unearned
|
|
78
|
+
* (u-msdsmkid mechanism b; the 3 archived sheds were all on step-red runs).
|
|
101
79
|
* @returns {{ ok: boolean, reds: Red[], plan: object|null }}
|
|
102
80
|
*/
|
|
103
|
-
export function validatePlan(input: object | string, { job, maxStepRounds, scopes,
|
|
81
|
+
export function validatePlan(input: object | string, { job, maxStepRounds, scopes, seedRed, priorChecks }?: {
|
|
104
82
|
job?: any;
|
|
105
83
|
maxStepRounds?: number;
|
|
106
84
|
scopes?: string[];
|
|
107
|
-
|
|
85
|
+
seedRed?: string[];
|
|
86
|
+
priorChecks?: string[];
|
|
108
87
|
}): {
|
|
109
88
|
ok: boolean;
|
|
110
89
|
reds: Red[];
|
|
@@ -120,7 +99,6 @@ export const MAX_EXITS_PER_STEP: 2;
|
|
|
120
99
|
/** plan size bound — per-step rounds bound the spend, but an unbounded step
|
|
121
100
|
* COUNT is still an unbounded claim on the wallet; same 8 as loop.maxIterations */
|
|
122
101
|
export const MAX_PLAN_STEPS: 8;
|
|
123
|
-
export { WRITE_VERBS };
|
|
124
102
|
/** scope-menu bound: the menu is a PROMPT ingredient on every draft and
|
|
125
103
|
* redraft, so an unbounded repo would price the drafting call by its directory
|
|
126
104
|
* count. Shallow entries survive the cap (see `legalScopes`) because they are
|
|
@@ -130,7 +108,27 @@ export const MAX_SCOPE_MENU: 24;
|
|
|
130
108
|
* construction — the agent picks a tier, never names an arbitrary model string
|
|
131
109
|
* (the tier→model mapping and any effort params are the RUNNER's, per provider).
|
|
132
110
|
* opus is deliberately absent: reserved for fresh builds hamr explicitly assigns,
|
|
133
|
-
* never plan-selectable.
|
|
111
|
+
* never plan-selectable.
|
|
112
|
+
*
|
|
113
|
+
* 2026-08-06 — haiku dropped, on hamr's order: "haiku should be dropped to see if
|
|
114
|
+
* it passes, if it's agent poor work or harness, like always." This is a REVERSIBLE
|
|
115
|
+
* ATTRIBUTION PROBE, not a finding that haiku is incapable. What the $0 archive read
|
|
116
|
+
* (186 spines / 71 accepted plans / 255 steps) actually shows is a CONFOUND, in both
|
|
117
|
+
* directions: only 20 steps ever declared a tier at all — haiku 12 (9 of them on a
|
|
118
|
+
* REPLANNED plan, 6 the LAST step of their plan, bounds 3–12 rounds, 2 ever green)
|
|
119
|
+
* against explicitly-declared sonnet 8 (bounds 14–32, 5 green). The planner picks
|
|
120
|
+
* haiku for steps it judges mechanical, and replanned steps sit on runs already in
|
|
121
|
+
* trouble, so neither column is a controlled read. All three bareagent-u runs of
|
|
122
|
+
* 2026-08-06 tiered the REPLANNED step down (haiku/6, haiku/8, haiku/12) and all
|
|
123
|
+
* three failed, against sonnet initial steps at 30–40 rounds — a confound sitting
|
|
124
|
+
* on the programme's most recent capability read. Removing the tier is how it gets
|
|
125
|
+
* attributed: with only one tier expressible, a still-failing replan is agent or
|
|
126
|
+
* harness, not the tier.
|
|
127
|
+
*
|
|
128
|
+
* The `model` FIELD and its validator branch stay: a one-entry menu means a plan (or
|
|
129
|
+
* a stored bridge) declaring `model:"sonnet"` still validates, and restoring haiku is
|
|
130
|
+
* a one-token edit. This narrows only what the AGENT may express — `--model haiku`
|
|
131
|
+
* remains the OPERATOR's probe knob (PRD v1.36), untouched in the runner scripts. */
|
|
134
132
|
export const STEP_MODELS: readonly string[];
|
|
135
133
|
export type Red = {
|
|
136
134
|
code: string;
|
|
@@ -139,3 +137,5 @@ export type Red = {
|
|
|
139
137
|
verb?: string;
|
|
140
138
|
};
|
|
141
139
|
import { WRITE_VERBS } from './job.js';
|
|
140
|
+
import { hasNestedQuantifier } from './validate.js';
|
|
141
|
+
export { WRITE_VERBS, hasNestedQuantifier };
|