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 +76 -1
- package/bareloop.context.md +3 -2
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/job.js +12 -1
- package/src/planrun.js +8 -1
- package/src/trend.js +25 -10
- package/types/index.d.ts +1 -1
- package/types/trend.d.ts +9 -1
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
|
-
## [
|
|
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
|
|
package/bareloop.context.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
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
|
|
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
|