bareloop 0.17.0 → 0.19.1

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,7 +5,82 @@ 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]
8
+ ## [0.19.1] — 2026-09-02
9
+
10
+ ### Fixed
11
+
12
+ - `tests/no-test-sniffing.test.js` no longer runs the close against the machine-local
13
+ `pulselog-u` patient — that test could only pass on the maintainer's machine and
14
+ turned CI and the v0.19.0 publish red (v0.19.0 was tagged but never published).
15
+
16
+ ## [0.19.0] — 2026-09-02
17
+
18
+ _Tagged 2026-09-02, never published: `publish.yml` failed on a machine-local test; see 0.19.1._
19
+
20
+ ### Added
21
+
22
+ - **G3 bench row (the structural negative).** `scripts/u-pulselog-close.mjs`
23
+ gains an additive `no-test-sniffing` close stage (planted-contradiction cheat catcher,
24
+ modelled on `no-suppressions`) and `--workdir`/`--seed` argv flags so the same close
25
+ script can judge a second patient without touching `jobs/pulselog-u-types.json`'s
26
+ behaviour or signed hash. New spec `jobs/pulselog-g3-types.json`, registered in
27
+ `scripts/run-u.mjs`. Frozen 2026-09-01 (hamr: "freeze"), signed and established
28
+ 2026-09-01 — establishing run `u-mties136` read non-green as pre-registered
29
+ (`step-red`, $2.98 of $4, 21.9 of 25 min) — see `docs/product/G3-SCOPING.md`, the
30
+ BENCH-PREREG freeze amendment, and FINDINGS F125.
31
+ - **`tokens` tail line in `scripts/run-u.mjs`'s run readout** (hamr's order,
32
+ 2026-09-01). `job-end` carries dollars only; tokens existed only per round on
33
+ `worker-round.usage`/`judge-round.usage`, invisible in the printed tail. New
34
+ `tokensLine`/`fmtTokens` exports in `scripts/u-readout.mjs` sum `usage`
35
+ (`inputTokens`/`outputTokens`/`cacheReadTokens`/`cacheCreationTokens`) over every
36
+ `worker-round` and `judge-round` record of the CURRENT leg's own spine —
37
+ `worker-result` (an attempt-level echo of round sums) is deliberately excluded. A
38
+ round with no `usage` object (every `judge-round` today) counts as UNKNOWN and is
39
+ named in a trailing `(N round(s) unpriced/no usage)` note rather than folded into
40
+ the total as 0. Prints right after the `rounds` line. 5 new tests.
41
+
42
+ ### Fixed
43
+
44
+ - **`scripts/run-u.mjs`'s run tail pluralizes `plan N step(s)`.** A 1-step plan
45
+ previously printed `1 steps`; the tail now reads `1 step` / `N steps` correctly.
46
+
47
+ ## [0.18.0] — 2026-08-31
48
+
49
+ ### Added
50
+
51
+ - **Close stages declare their comparison direction (F120).** `job-v1` close stages gain an
52
+ optional `direction: 'up'|'down'` field; absent means `'down'` (unchanged behaviour for
53
+ every existing close). `src/trend.js`'s close-fix governor hardcoded lower-is-better —
54
+ wrong for a rate-shaped close, where up is better — and cap-halted a converging G2 run
55
+ for improving (live on `u-mtgr1qnu`); a mirror defect funded a collapsing rate as
56
+ progress. Direction is never inferred from goal prose and never LLM-judged; a new
57
+ `isBetter(stage,a,b)` helper in `src/trend.js` drives `countImproved` and both
58
+ `best`-tracking sites, `createTrend` gains a `directions` option, `STAGE_FIELDS`
59
+ (`src/job.js`) validates the field against an allow-list. `scripts/run-u.mjs` prints each
60
+ stage's direction at the close-stage banner. 7 new tests, each proven failing against the
61
+ old code first; both production job hashes independently re-verified unchanged.
62
+
63
+ ### Changed
64
+
65
+ - **`--read-shim` defaults to A1 (cap) in `scripts/run-u.mjs`** (hamr's call, 2026-08-31).
66
+ `--read-shim off` restores A0. The three frozen bench rows in `docs/product/BENCH-PREREG.md`
67
+ pin `off` explicitly, so their baselines keep the condition they were established under —
68
+ no bench re-baseline required or paid.
69
+
70
+ ### Fixed
71
+
72
+ - **`resolveWorkerModel` is now exported from the package root.** `bareloop.context.md`
73
+ names it as the spec-model resolution mechanism, but v0.17.0 shipped it reachable only
74
+ in-repo (`src/job.js`) — the exports map admits only `.`, so an adopter could not import
75
+ it. Documented-but-unexported is a false contract (same rule that exported `checkMenu`);
76
+ caught by validating the installed 0.17.0 artifact from the registry. Additive only.
77
+ - **G2 cold-variant `clean-run` close dropped a vacuous stage.** The borrowed
78
+ `changed-from-seed` stage compared against a seed manifest that can never show a change
79
+ on a genuinely cold patient (zero files under `tests/testgen/` at seed), firing a spurious
80
+ `unchanged-red` before the worker had written anything. New
81
+ `scripts/testgen-cold-check-close.mjs` (grader-identical minus that stage);
82
+ `jobs/aurora-testgen-cold.json`'s `clean-run` stage repoints to it. Live-proven: the
83
+ re-establish run recorded zero `unchanged-red` events.
9
84
 
10
85
  ## [0.17.0] — 2026-08-31
11
86
 
@@ -147,7 +147,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
147
147
  |---|---|---|
148
148
  | `goal` | non-empty text | what the agent plans against |
149
149
  | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three are ADMITTED today** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself. `soft-green` has its judged floor (`judged-floor`, signed rubric card + calibration gate) and is the forward path for a job needing judgement, paired with the end-of-run review door; `hitl` is LEGACY — retired as a design direction (see the direction note at the top) but still admitted and still works exactly as documented here. Every `request-red` (e.g. for a locked TOOL verb, not a verdict class) also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
150
- | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
150
+ | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs?, direction? }, ...]` (PRD v1.28), or a close object (table below) | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
151
151
  | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params }], notes? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
152
152
  | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
153
153
  | `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
@@ -203,8 +203,9 @@ contract the single close object has always used) plus:
203
203
  | stage field | shape | notes |
204
204
  |---|---|---|
205
205
  | `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
206
- | `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the ten specs in `jobs/`, four hide a final `verdict` stage and six hide a `changed-from-seed` precondition instead (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
206
+ | `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the fourteen specs in `jobs/`, five hide a final `verdict` stage and seven hide a `changed-from-seed` precondition instead (the remaining two use an object-form close with no staged `offer` flags) (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
207
207
  | `needs` | optional non-empty array of EARLIER stage names | a stage that reads what an earlier stage built (e.g. "the public API matches what an earlier stage emitted") names its prerequisite chain; picking it via `check-passes` runs the chain first, then the stage itself. Every name must be declared before this stage (`invalid-value` otherwise); `needs` + `offer: false` together is incoherent (`invalid-value`) — a stage with a chain to run must be reachable |
208
+ | `direction` | optional `'up'` \| `'down'`, allow-list validated (anything else is `invalid-value`) | which way this stage's numeric series is READING as progress — `'down'` (fewer is better: typecheck errors, suppressions) or `'up'` (more is better: a fault-detection rate). **Absent means `'down'`** — every close built before this field existed is a count-shaped close, so the silent default is behaviour-identical for all of them (F120). Never inferred from the goal text and never LLM-judged — prose the agent writes must never influence its own halt rule, the same reasoning that keeps every close hand-authored or declared, never judged. Consulted by `isBetter(stage,a,b)` in `src/trend.js`, the one place the close-fix governor's `countImproved` and `best`-tracking read direction; `scripts/run-u.mjs` prints each stage's direction at the close-stage banner so a silent default is never invisible |
208
209
 
209
210
  The stages run in declared order as the close itself; the first red renders the verdict and
210
211
  later stages never run. `checkMenu(close)` returns only the offerable stages (each with its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.17.0",
3
+ "version": "0.19.1",
4
4
  "description": "Workflows that earn their own design, with receipts \u2014 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",
package/src/index.js CHANGED
@@ -24,7 +24,7 @@ export { makeSpine } from './spine.js';
24
24
  // name. Documented-but-unexported is a false contract; the additions are additive.
25
25
  export { ralph, runClose, runStages, CLOSE_FAULTS } from './ralph.js';
26
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';
27
+ export { validateJob, jobSpecHash, checkApproval, checkMenu, resolveWorkerModel, 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';
28
28
  // Layer 2: the plan-v1 validator — the agent-authored plan doc's gate; the
29
29
  // two-doc split's third validator never happens (plan-v1 gates the PLAN, the
30
30
  // job spec stays the arbiter's only home).
package/src/job.js CHANGED
@@ -492,7 +492,7 @@ export function checkMenu(close) {
492
492
  }
493
493
 
494
494
  /** the fields a close STAGE may carry — anything else is a smuggle channel */
495
- const STAGE_FIELDS = ['name', 'cmd', 'expect', 'judged', 'gapKeep', 'offer', 'needs'];
495
+ const STAGE_FIELDS = ['name', 'cmd', 'expect', 'judged', 'gapKeep', 'offer', 'needs', 'direction'];
496
496
 
497
497
  /**
498
498
  * Validate the staged close. Every stage is a predicate BODY under the same
@@ -534,6 +534,17 @@ function validateStagedClose(stages, red) {
534
534
  if (bad.length) red('invalid-value', `${at}.needs`, `must name stages declared BEFORE this one (a prerequisite that has not run yet cannot be one): ${bad.join(', ')} — earlier stages: ${[...earlier].join(', ') || 'none'}`);
535
535
  }
536
536
  }
537
+ // the close-fix governor's comparison direction (2026-08-31 ruling, hamr's
538
+ // option A): a signed, per-stage field, never inferred from goal prose and
539
+ // never LLM-judged. 'up' = higher is better (a rate of good things caught,
540
+ // e.g. mutation-kill %); 'down' = lower is better (a count of bad things
541
+ // remaining, e.g. typecheck errors or suppressions). Absent means 'down' —
542
+ // the field defaults to the trend reader's pre-existing behaviour exactly,
543
+ // so every close authored before this field existed keeps an IDENTICAL
544
+ // spec hash (an absent field adds nothing to the hash).
545
+ if (s.direction !== undefined && s.direction !== 'up' && s.direction !== 'down') {
546
+ red('invalid-value', `${at}.direction`, `"up" or "down" — up means higher is better (a rate of good things caught), down means lower is better (a count of bad things remaining); omit for down (the default)`);
547
+ }
537
548
  });
538
549
  }
539
550
 
package/src/planrun.js CHANGED
@@ -1363,7 +1363,13 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
1363
1363
  * an unchanged tree is flat on its own evidence while the RUN — the thing the human
1364
1364
  * is deciding about — may have been converging when its allowance ran out. Cold, the
1365
1365
  * seed is empty and this is byte-identical to the pre-resume reader. */
1366
- const runTrend = createTrend({ stageOrder: stagedClose.map((/** @type {any} */ s) => s.name), seed: resumeGrades });
1366
+ // per-stage comparison direction, read straight off the signed close (2026-08-31
1367
+ // ruling): a stage absent here — every close authored before this field existed
1368
+ // included — resolves to 'down' inside createTrend, so this is additive only.
1369
+ const stageDirections = Object.fromEntries(
1370
+ stagedClose.filter((/** @type {any} */ s) => s?.direction === 'up' || s?.direction === 'down').map((/** @type {any} */ s) => [s.name, s.direction]),
1371
+ );
1372
+ const runTrend = createTrend({ stageOrder: stagedClose.map((/** @type {any} */ s) => s.name), seed: resumeGrades, directions: stageDirections });
1367
1373
  /** the run-level MONEY record, one shape wherever the wallet stops the run —
1368
1374
  * W-2's `emitWallHalt` in a money coat, and deliberately its mirror image. hamr's
1369
1375
  * ruling for TIME was "keep the grade we already have and stop"; a money cut is
@@ -3396,6 +3402,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
3396
3402
  stageOrder: stagedClose.map((/** @type {any} */ s) => s.name),
3397
3403
  limit: FIX_STRIKE_LIMIT,
3398
3404
  blindCap: capRuns,
3405
+ directions: stageDirections,
3399
3406
  });
3400
3407
  fixTrend.record({ gap: post.gap ?? '' });
3401
3408
  /** ralph's `ladder` seam, filled by the trend reader instead of the step
package/src/trend.js CHANGED
@@ -212,12 +212,25 @@ const UNSTAGED = '(unstaged)';
212
212
  * grades a PREVIOUS leg of this same run already recorded, oldest first, read off
213
213
  * its own spine. Baselines only (see THE SEED above). Empty/omitted is the cold
214
214
  * path and is byte-identical to the pre-resume reader.
215
+ * @param {Record<string, 'up'|'down'>} [opts.directions] per-stage comparison
216
+ * direction, read from the signed close spec (2026-08-31 ruling, hamr's option
217
+ * A) — never inferred, never LLM-judged. 'up' = higher is better (a rate of good
218
+ * things caught); 'down' = lower is better (a count of bad things remaining, the
219
+ * pre-existing behaviour). A stage absent from this map — including every close
220
+ * authored before this field existed — resolves to 'down', so a cold spec's
221
+ * trend reading is byte-identical to before this option existed.
215
222
  */
216
- export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCap = null, seed = [] } = {}) {
223
+ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCap = null, seed = [], directions = {} } = {}) {
217
224
  /** @type {Map<string, number[]>} stage → every count it reported, in order */
218
225
  const series = new Map();
219
- /** @type {Map<string, number>} stage → the BEST (lowest) count it ever reported */
226
+ /** @type {Map<string, number>} stage → the BEST (max for 'up', min for 'down') count it ever reported */
220
227
  const best = new Map();
228
+ /** is `a` a BETTER reading than `b` for this stage, under its own declared
229
+ * direction? 'up' stages are better when higher (a caught-rate climbing);
230
+ * every other stage — including one absent from `directions` — is better
231
+ * when lower, which is the ORIGINAL, only comparison this reader ever made. */
232
+ const isBetter = (/** @type {string} */ stage, /** @type {number} */ a, /** @type {number} */ b) =>
233
+ (directions[stage] === 'up' ? a > b : a < b);
221
234
  /** the furthest stage index reached so far; null until a KNOWN stage is seen */
222
235
  let bestStageIdx = null;
223
236
  let iterations = 0;
@@ -249,7 +262,7 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
249
262
  if (!series.has(st)) series.set(st, []);
250
263
  /** @type {number[]} */ (series.get(st)).push(v);
251
264
  const b = best.get(st);
252
- if (b === undefined || v < b) best.set(st, v);
265
+ if (b === undefined || isBetter(st, v, b)) best.set(st, v);
253
266
  }
254
267
 
255
268
  return {
@@ -273,7 +286,7 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
273
286
  // ── the two comparisons, each against this stage's OWN history ──
274
287
  const priorBest = best.get(st);
275
288
  const countComparable = v !== null && priorBest !== undefined;
276
- const countImproved = countComparable && v < /** @type {number} */ (priorBest);
289
+ const countImproved = countComparable && isBetter(st, /** @type {number} */ (v), /** @type {number} */ (priorBest));
277
290
  // the ordering signal only exists between two KNOWN positions that DIFFER —
278
291
  // an unnamed stage has no position, and standing still says nothing
279
292
  const stageComparable = idx >= 0 && bestStageIdx !== null && idx !== bestStageIdx;
@@ -286,7 +299,7 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
286
299
  if (v !== null) {
287
300
  if (!series.has(st)) series.set(st, []);
288
301
  /** @type {number[]} */ (series.get(st)).push(v);
289
- if (priorBest === undefined || v < priorBest) best.set(st, v);
302
+ if (priorBest === undefined || isBetter(st, v, priorBest)) best.set(st, v);
290
303
  }
291
304
  if (idx >= 0 && (bestStageIdx === null || idx > bestStageIdx)) bestStageIdx = idx;
292
305
  if (stageImproved) advancedEver = true;
@@ -348,10 +361,12 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
348
361
  // question is the different one of whether the LAST attempt helped.
349
362
  const numeric = rows.filter((r) => r.values.length > 1);
350
363
  const render = numeric.map((r) => `${r.stage} ${r.values.join(' → ')}`).join('; ');
351
- // convergence is defined PER STAGE and per stage only: some axis is lower than
352
- // where it started. First-vs-last, because the question a halt asks is "did
353
- // this run make net progress", not "did the last attempt help".
354
- const fell = numeric.some((r) => r.values[r.values.length - 1] < r.values[0]);
364
+ // convergence is defined PER STAGE and per stage only: some axis moved toward
365
+ // its OWN declared better direction from where it started — lower for a
366
+ // 'down' stage (a count of bad things remaining), higher for an 'up' stage (a
367
+ // rate of good things caught). First-vs-last, because the question a halt asks
368
+ // is "did this run make net progress", not "did the last attempt help".
369
+ const improvedNet = numeric.some((r) => isBetter(r.stage, r.values[r.values.length - 1], r.values[0]));
355
370
 
356
371
  // No stage ever reported a comparable number: the honest answer, stated rather
357
372
  // than rounded up to either direction. A "flat" here would recommend rewriting
@@ -385,7 +400,7 @@ export function createTrend({ stageOrder = [], limit = FIX_STRIKE_LIMIT, blindCa
385
400
  // Past this point a real number decided it, so motion is not consulted and is
386
401
  // not reported: a byte comparison beside a measured direction can only ever
387
402
  // contradict it, and the contradiction would be the weaker signal winning.
388
- return fell
403
+ return improvedNet
389
404
  ? {
390
405
  trend: 'converging',
391
406
  motion: null,
package/types/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { makeSpine } from "./spine.js";
2
2
  export { runPlan } from "./planrun.js";
3
3
  export { ralph, runClose, runStages, CLOSE_FAULTS } from "./ralph.js";
4
4
  export { globToPrefix, scanSecrets } from "./validate.js";
5
- 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";
5
+ export { validateJob, jobSpecHash, checkApproval, checkMenu, resolveWorkerModel, 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";
6
6
  export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS, closeStagesOf } from "./plan.js";
7
7
  export { snapshotScope, evalExits } from "./exits.js";
8
8
  export { runJob, ACCOUNTED_ROUND_TYPES } from "./run.js";
package/types/trend.d.ts CHANGED
@@ -30,8 +30,15 @@ export function readGrade(gap?: string): {
30
30
  * grades a PREVIOUS leg of this same run already recorded, oldest first, read off
31
31
  * its own spine. Baselines only (see THE SEED above). Empty/omitted is the cold
32
32
  * path and is byte-identical to the pre-resume reader.
33
+ * @param {Record<string, 'up'|'down'>} [opts.directions] per-stage comparison
34
+ * direction, read from the signed close spec (2026-08-31 ruling, hamr's option
35
+ * A) — never inferred, never LLM-judged. 'up' = higher is better (a rate of good
36
+ * things caught); 'down' = lower is better (a count of bad things remaining, the
37
+ * pre-existing behaviour). A stage absent from this map — including every close
38
+ * authored before this field existed — resolves to 'down', so a cold spec's
39
+ * trend reading is byte-identical to before this option existed.
33
40
  */
34
- export function createTrend({ stageOrder, limit, blindCap, seed }?: {
41
+ export function createTrend({ stageOrder, limit, blindCap, seed, directions }?: {
35
42
  stageOrder?: string[] | undefined;
36
43
  limit?: number | undefined;
37
44
  blindCap?: number | null | undefined;
@@ -39,6 +46,7 @@ export function createTrend({ stageOrder, limit, blindCap, seed }?: {
39
46
  stage?: string | null;
40
47
  value?: number | null;
41
48
  }[] | undefined;
49
+ directions?: Record<string, "up" | "down"> | undefined;
42
50
  }): {
43
51
  /**
44
52
  * Read one RED close grade. A green never reaches here (both callers stop on