clearotron 0.3.0 → 0.3.1-beta.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/bin/example.mjs +28 -15
- package/bin/onboard.mjs +7 -1
- package/bin/start.mjs +13 -4
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +20 -2
- package/driver/CHANGELOG.md +10 -0
- package/driver/band-size.mjs +66 -0
- package/driver/corrections-feedforward.mjs +128 -5
- package/driver/demo-container.mjs +83 -6
- package/driver/gateway.mjs +3 -3
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +9 -9
- package/driver/pipeline.mjs +113 -28
- package/driver/portal-service.mjs +8 -1
- package/driver/repair-composers.mjs +8 -0
- package/driver/stages.mjs +13 -1
- package/driver/suite-census.json +38 -14
- package/mcp-server/CHANGELOG.md +6 -0
- package/mcp-server/lib/ops.mjs +10 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +13 -2
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/release-version.mjs +84 -4
- package/shared/trigger-lane.mjs +66 -0
- package/shared/wsl.mjs +20 -3
package/bin/example.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
46
46
|
import { spawn } from "node:child_process";
|
|
47
47
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
48
48
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
49
|
-
import { isFrozen, demoChildren,
|
|
49
|
+
import { isFrozen, demoChildren, demoInventory, prepareSample } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
|
|
50
50
|
import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.mjs";
|
|
51
51
|
|
|
52
52
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -109,7 +109,10 @@ const DEMO_ROOT = join(REPO, "demo");
|
|
|
109
109
|
// --run-dir takes a directory outright. --product names a child. Neither given: EVERY child, and the
|
|
110
110
|
// names are PRINTED below rather than assumed, because "the demo" means several.
|
|
111
111
|
const wanted = flag("--product");
|
|
112
|
-
|
|
112
|
+
// EVERY SAMPLE THE CONTAINER HOLDS, the unusable ones named (driver/demo-container.mjs says why).
|
|
113
|
+
const inventory = demoInventory(DEMO_ROOT);
|
|
114
|
+
const children = inventory.children;
|
|
115
|
+
const ALL = !flag("--run-dir") && !wanted;
|
|
113
116
|
|
|
114
117
|
// ── ALL OF THEM, UNLESS THE CALLER NARROWED IT ──────────────────────────────────────────────────────
|
|
115
118
|
//
|
|
@@ -129,6 +132,9 @@ const sampleDirs = flag("--run-dir")
|
|
|
129
132
|
// The refusal below is about ONE directory, and with nothing shipped there is no directory to name — so
|
|
130
133
|
// the container itself is what it looks at, which is what it always did when `demo/` was empty.
|
|
131
134
|
const sampleDir = sampleDirs[0] ?? resolve(DEMO_ROOT);
|
|
135
|
+
// EVERY SAMPLE UNUSABLE is the absence below with its reasons, not "no frozen demo" over a full container.
|
|
136
|
+
if (ALL && !sampleDirs.length && inventory.unusable.length)
|
|
137
|
+
die("demo: no demo could be replayed.", "", ...inventory.unusable.map((u) => ` ${u.name}: ${u.why}`));
|
|
132
138
|
if (!sampleDirs.length || !isFrozen(sampleDir)) {
|
|
133
139
|
// AN ABSENCE IS A FINDING, AND IT NAMES WHAT IT LOOKED AT. This exits 1 and always has; a report of
|
|
134
140
|
// it exiting 0 did not reproduce at v0.1.0 or at main's tip. An arm pins it.
|
|
@@ -153,15 +159,22 @@ if (!sampleDirs.length || !isFrozen(sampleDir)) {
|
|
|
153
159
|
// PUBLISHING WRITES A RECEIPT INTO THE RUN DIRECTORY, and `demo/` is tracked — so a reader who only READ
|
|
154
160
|
// the demo came back to a dirty checkout. `publishSource` is the one definition of that rule, shared with
|
|
155
161
|
// the launcher, which seeds the pool from the same container on every `--demo` start.
|
|
156
|
-
// EVERY ONE THAT WAS ASKED FOR IS READ BEFORE ANY IS PUBLISHED
|
|
157
|
-
// refusal about
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
});
|
|
162
|
+
// EVERY ONE THAT WAS ASKED FOR IS READ BEFORE ANY IS PUBLISHED. One named by --product or --run-dir that
|
|
163
|
+
// cannot be used is a refusal about it by name. With every sample asked for, ONE THAT CANNOT BE USED MUST
|
|
164
|
+
// NOT COST THE OTHERS: it joins the failures the replay below collects, named with its reason, and the
|
|
165
|
+
// rest are published. An unreadable file inside one sample used to throw out of the copy here and take
|
|
166
|
+
// every demo down with a stack trace (measured on a published beta, 2026-09-11).
|
|
167
|
+
const failures = ALL ? inventory.unusable.map((u) => ({ name: u.name, why: u.why })) : [];
|
|
168
|
+
const samples = [];
|
|
169
|
+
for (const dir of sampleDirs) {
|
|
170
|
+
const r = prepareSample(dir, { repoRoot: REPO });
|
|
171
|
+
if (r.sample) samples.push(r.sample);
|
|
172
|
+
else if (ALL) failures.push({ name: r.name, why: r.why });
|
|
173
|
+
else die(`demo: ${dir} cannot be replayed — ${r.why}.`);
|
|
174
|
+
}
|
|
175
|
+
if (!samples.length) die("demo: no demo could be replayed.", "", ...failures.map((f) => ` ${f.name}: ${f.why}`));
|
|
176
|
+
// How many samples this tree ships: the ones replayed and the ones named as failures, never the readable ones.
|
|
177
|
+
const shipped = samples.length + failures.length;
|
|
165
178
|
const publishFrom = samples[0].publishFrom;
|
|
166
179
|
const meta = samples[0].meta;
|
|
167
180
|
|
|
@@ -226,7 +239,7 @@ if (existsSync(poolRoot) && !statSync(poolRoot).isDirectory()) die(`demo: ${pool
|
|
|
226
239
|
console.log(`\n ${BRAND.name} ${BRAND.product.toLowerCase()} — demo\n`);
|
|
227
240
|
console.log(samples.length === 1
|
|
228
241
|
? ` sample: ${samples[0].dir}`
|
|
229
|
-
: ` samples: ${samples.length} — ${samples.map((x) => x.name).join(", ")}`);
|
|
242
|
+
: ` samples: ${samples.length}${failures.length ? ` of ${shipped}` : ""} — ${samples.map((x) => x.name).join(", ")}`);
|
|
230
243
|
console.log(` reports folder: ${poolRoot}\n`);
|
|
231
244
|
|
|
232
245
|
mkdirSync(poolRoot, { recursive: true });
|
|
@@ -242,7 +255,6 @@ const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publis
|
|
|
242
255
|
// The failures are collected and reported together at the end, and the process exits non-zero, because a
|
|
243
256
|
// demo that came up missing a quarter of itself is not a success however good the three look.
|
|
244
257
|
const results = [];
|
|
245
|
-
const failures = [];
|
|
246
258
|
for (const s0 of samples) {
|
|
247
259
|
try {
|
|
248
260
|
// poolUrl "" on purpose: the report's own link block is for a deployment that serves the pool at a
|
|
@@ -291,11 +303,12 @@ const spineOf = (pub) =>
|
|
|
291
303
|
evidence, on ${pub.receipts.citing}/${pub.receipts.marks} mark(s)`
|
|
292
304
|
: `this lane's publisher reported no finding count — the report itself is the record`;
|
|
293
305
|
for (const r of results) console.log(` published: ${r.published.runId}\n ${r.name} — ${spineOf(r.published)}`);
|
|
294
|
-
|
|
306
|
+
// "ONE PER PRODUCT" ONLY WHEN IT IS TRUE: with a sample missing, the count below says how many of how many.
|
|
307
|
+
if (results.length > 1 && !failures.length) console.log(`\n ${results.length} demo reports are published and listed — one per product.`);
|
|
295
308
|
// LOUD, AND ON STDERR, AND NON-ZERO. Said after the successes so a reader sees what they DID get first,
|
|
296
309
|
// and cannot mistake the run for a clean one.
|
|
297
310
|
if (failures.length) {
|
|
298
|
-
console.error(`\n ${failures.length} of ${
|
|
311
|
+
console.error(`\n ${failures.length} of ${shipped} demo(s) could NOT be replayed:`);
|
|
299
312
|
for (const f of failures) console.error(` ${f.name}: ${f.why}`);
|
|
300
313
|
console.error(` The portal below lists the ${results.length} that published. This exits non-zero.`);
|
|
301
314
|
process.exitCode = 1;
|
package/bin/onboard.mjs
CHANGED
|
@@ -2579,8 +2579,14 @@ export async function runCheck() {
|
|
|
2579
2579
|
// returns null when the header is absent — a looked-and-none answer, not a did-not-look — and
|
|
2580
2580
|
// the readers separate those, so a probe that omits the field reads as never-looked rather
|
|
2581
2581
|
// than silently as "no challenge".
|
|
2582
|
+
// AND SO DOES THE REFUSAL'S OWN SENTENCE, for the same reason one line up: absence and
|
|
2583
|
+
// did-not-look are different answers. A proxy-fronted door and a key door both refuse with 401
|
|
2584
|
+
// and no challenge header, so the body is what separates them — doctor must read it too, or it
|
|
2585
|
+
// would answer this question differently from the portal off the same shared verdict.
|
|
2586
|
+
let body = null;
|
|
2587
|
+
if (res.status === 401) { try { body = (await res.text()).slice(0, 400); } catch { body = null; } }
|
|
2582
2588
|
probe = { ok: res.status < 500, status: res.status, error: null,
|
|
2583
|
-
challenge: res.headers.get("www-authenticate") };
|
|
2589
|
+
challenge: res.headers.get("www-authenticate"), body };
|
|
2584
2590
|
} catch (e) { probe = { ok: false, status: null, error: String(e?.cause?.code ?? e?.name ?? e?.message ?? e) }; }
|
|
2585
2591
|
}
|
|
2586
2592
|
// THE PREFIX TRAVELS WITH THE MESSAGE. Doctor's own guard runs every command
|
package/bin/start.mjs
CHANGED
|
@@ -1435,8 +1435,11 @@ if (isMain) {
|
|
|
1435
1435
|
// SEEDED FROM A COPY, for the reason the player publishes from one: republishing writes a receipt
|
|
1436
1436
|
// into the run directory it reads, and `demo/` is tracked. This is the path a reader actually takes
|
|
1437
1437
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1438
|
-
|
|
1439
|
-
|
|
1438
|
+
// ONE SAMPLE AT A TIME: one whose files cannot be read is left out and named below, and the others
|
|
1439
|
+
// seed. Copied in one call, a single unreadable file emptied the whole archive.
|
|
1440
|
+
const { publishContainer, seedDemoRuns } = await import("../driver/demo-container.mjs");
|
|
1441
|
+
const container = publishContainer(join(REPO, "demo"), { repoRoot: REPO });
|
|
1442
|
+
const seed = await seedPool({ pool: paths.pool, examplesDir: container.dir, republish: republishRun });
|
|
1440
1443
|
// AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
|
|
1441
1444
|
// the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
|
|
1442
1445
|
// links are stamped with this portal's address, the one the Open line prints.
|
|
@@ -1461,10 +1464,16 @@ if (isMain) {
|
|
|
1461
1464
|
}
|
|
1462
1465
|
// Never a silent nothing. "The archive is empty" and "the archive is empty and nobody noticed why"
|
|
1463
1466
|
// look identical in the browser, so both other outcomes are said out loud.
|
|
1464
|
-
|
|
1467
|
+
// A SAMPLE LEFT OUT IS NAMED ONCE, by the first copy that could not take it (its report or its run).
|
|
1468
|
+
const leftOut = new Map();
|
|
1469
|
+
for (const u of [...container.unusable, ...runs.failed]) if (!leftOut.has(u.name)) leftOut.set(u.name, u.why);
|
|
1470
|
+
// The seeder counts "every example this package ships" off the copy it was handed, so with a sample
|
|
1471
|
+
// left out that sentence would name the copy's count as the package's; the warnings below say instead.
|
|
1472
|
+
if (seed.skipped && !leftOut.size) say(` archive ${seed.skipped}`);
|
|
1465
1473
|
for (const p of seed.problems) err(` WARNING: sample seeding — ${p}`);
|
|
1474
|
+
for (const [name, why] of leftOut) err(` WARNING: demo sample ${name} left out — ${why}`);
|
|
1466
1475
|
} catch (e) {
|
|
1467
|
-
err(` WARNING: the example report could not be seeded (${String(e?.message ?? e)}) — the archive will come up empty. Everything else works; \`
|
|
1476
|
+
err(` WARNING: the example report could not be seeded (${String(e?.message ?? e)}) — the archive will come up empty. Everything else works; \`${invoke("demo")}\` shows a sample without touching this install.`);
|
|
1468
1477
|
}
|
|
1469
1478
|
|
|
1470
1479
|
let roster = [];
|
package/build-info.json
CHANGED
package/docs/DELIVERY.md
CHANGED
|
@@ -136,8 +136,26 @@ drives the whole delivery side over the ops MCP face — mint its token verb-sco
|
|
|
136
136
|
`sendPending`, clears the marker — idempotent). **A settle is a receipt, not an intention:** with
|
|
137
137
|
neither `messageId` nor an `attestation` it REFUSES, and if the send was blocked or
|
|
138
138
|
failed you do not call it at all — the marker stays and the send stays owed.
|
|
139
|
-
3. For
|
|
140
|
-
|
|
139
|
+
3. For `run-failed`: the requester is **owed** this notice exactly as they are owed a report. Route the
|
|
140
|
+
packet's ready-made `text`, then settle it with **`mark_sent`**, not `ack_event`. A failed run carries
|
|
141
|
+
`sendPending` until a send is confirmed, whichever lane wrote the packet, and `mark_sent` is the only
|
|
142
|
+
thing that clears it. Acknowledging the event instead removes the marker and leaves the run owed, so
|
|
143
|
+
the backstop scan re-arms it and the requester is told again on the next sweep.
|
|
144
|
+
4. For the remaining kinds — `intake-rejected`, `duplicate-skipped`, `late-bind-ack` — route the
|
|
145
|
+
packet's `text` → **`ack_event(file)`** (idempotent; validated as a bare `*.pending` name). These
|
|
146
|
+
describe something that did not become a run, so there is no run to settle.
|
|
147
|
+
|
|
148
|
+
**Poll at any hour.** Owed work does not keep office hours: a run that fails at 03:00 owes its requester
|
|
149
|
+
a notice at 03:00. Ask the door directly with **`list_runs({ sendPending: true })`** — the no-filesystem
|
|
150
|
+
equivalent of the backstop scan below, returning every run still owed a send, live or archived, with no
|
|
151
|
+
cap. An integrator that only runs during the day leaves a failure unreported until it next wakes, and
|
|
152
|
+
nothing in the product can compensate for that: the product composes the notice and records that it is
|
|
153
|
+
owed, and sending is yours.
|
|
154
|
+
|
|
155
|
+
**Two things that make that list incomplete, both silent.** A token scoped to named accounts sees only
|
|
156
|
+
those accounts' runs, so a run for an account the token does not carry is invisible rather than absent —
|
|
157
|
+
mint the integrator's token to cover every account it delivers for, and re-mint it when one is added.
|
|
158
|
+
And a `limit` you pass yourself is obeyed as given: for this query, do not pass one.
|
|
141
159
|
|
|
142
160
|
The filesystem loop below remains equivalent for integrators that do have data-plane access.
|
|
143
161
|
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.1-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a2c4b94: Fixed: A correction to a coverage note or an action is now applied, or the run records why it was not.
|
|
8
|
+
- b320c65: Fixed: A demo sample that cannot be read is named in the demo's output, and the other demos still publish.
|
|
9
|
+
- d9798db: Fixed: A clearance that stops now always records that its notice is still owed, so a failure cannot be passed over as already handled.
|
|
10
|
+
- ae2cc82: For operators: an assistant asking which searches still owe someone a notice now gets all of them, not just the fifty most recent. Asking for recent searches is unchanged.
|
|
11
|
+
- e390417: For operators: The portal's start-up check now says when its engine address is behind a sign-in it cannot pass, instead of reporting that address as reachable.
|
|
12
|
+
|
|
3
13
|
## 0.3.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// band-size.mjs — how big the register band was when a stage was dispatched against it.
|
|
5
|
+
//
|
|
6
|
+
// WHY A BUDGET NEEDS THIS BESIDE IT. Two judgment stages carry hand-set walls — 2700s and 2400s — and
|
|
7
|
+
// each was raised after a kill on one crowded matter. The numbers are in the stage table with their
|
|
8
|
+
// post-mortems, and not one of them records the BAND SIZE it was sized against, so the next person
|
|
9
|
+
// asking "is 2400 enough" has no denominator and re-derives it from an archived run by hand. That
|
|
10
|
+
// happened; it is why this exists.
|
|
11
|
+
//
|
|
12
|
+
// IT READS THE SHAPE, NOT THE BAND. `band-shape.json` is derived once per run and already carries the
|
|
13
|
+
// record and crowd totals, so the cost here is one small read rather than parsing the merged band on
|
|
14
|
+
// every dispatch. The band's own file supplies bytes, which is a stat.
|
|
15
|
+
//
|
|
16
|
+
// AN ABSENT BAND IS A NAMED REASON, NEVER A ZERO. A register-only run with nothing retrieved, a matter
|
|
17
|
+
// with no Nice classes (which compiles no register plan at all), and a replay of an archived run that
|
|
18
|
+
// predates the shape all reach a dispatch with no band on disk. Written as `0` those become a
|
|
19
|
+
// measurement saying the band was empty, which is a different and false claim — and every rollup that
|
|
20
|
+
// averages this field would silently take it. The same distinction `toolWaitByTool` draws: an object is
|
|
21
|
+
// a measurement, and the absence says which absence it is.
|
|
22
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
23
|
+
import { BAND_READING_STAGES } from "./stages.mjs";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The band this dispatch is about to be judged against, as a measurement or a named absence.
|
|
27
|
+
*
|
|
28
|
+
* @param {{bandShape: string, registerNamedBand: string}} paths the run's own paths (`ctx.paths`)
|
|
29
|
+
* @param {object} [io] injectable for tests — the real fs by default
|
|
30
|
+
* @returns {{records:number, crowds:number, bytes:number}|{absent:string}}
|
|
31
|
+
*/
|
|
32
|
+
export function bandSizeAtDispatch(paths, {
|
|
33
|
+
exists = existsSync, read = readFileSync, stat = statSync,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const shapePath = paths?.bandShape;
|
|
36
|
+
const bandPath = paths?.registerNamedBand;
|
|
37
|
+
if (!shapePath || !bandPath) return { absent: "no run paths" };
|
|
38
|
+
if (!exists(bandPath)) return { absent: "no merged register band on disk" };
|
|
39
|
+
// Bytes first: it is a stat, it cannot fail the way a parse can, and a band whose shape has not been
|
|
40
|
+
// derived yet is still a band whose size is worth recording.
|
|
41
|
+
let bytes = null;
|
|
42
|
+
try { bytes = stat(bandPath).size; } catch (e) { return { absent: `band unstatable (${e?.code ?? "unknown"})` }; }
|
|
43
|
+
if (!exists(shapePath)) return { absent: "no band shape derived yet", bytes };
|
|
44
|
+
let shape;
|
|
45
|
+
try { shape = JSON.parse(read(shapePath, "utf8")); }
|
|
46
|
+
catch (e) { return { absent: `band shape unreadable (${e?.code ?? "parse"})`, bytes }; }
|
|
47
|
+
const t = shape?.totals;
|
|
48
|
+
// A shape whose totals are not numbers is not a measurement of anything — say so rather than coercing.
|
|
49
|
+
if (!t || !Number.isFinite(t.records) || !Number.isFinite(t.crowds)) {
|
|
50
|
+
return { absent: "band shape carries no totals", bytes };
|
|
51
|
+
}
|
|
52
|
+
return { records: t.records, crowds: t.crowds, bytes };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The band size to record for `stage`, or undefined for a stage that does not read the band.
|
|
57
|
+
*
|
|
58
|
+
* THE DECISION LIVES HERE RATHER THAN AT THE CALL SITE, and that is the lesson of a plant that did not
|
|
59
|
+
* red: with the branch written inline in the dispatch, disabling the measurement outright changed
|
|
60
|
+
* nothing any test could see. A stage outside the set gets `undefined` and not a named absence, because
|
|
61
|
+
* "this stage does not read the band" is not a fact about the band.
|
|
62
|
+
*/
|
|
63
|
+
export function bandSizeForStage(stage, paths, io) {
|
|
64
|
+
if (!BAND_READING_STAGES.has(stage)) return undefined;
|
|
65
|
+
return bandSizeAtDispatch(paths, io);
|
|
66
|
+
}
|
|
@@ -55,6 +55,100 @@ export function targetsOf(flagText, known) {
|
|
|
55
55
|
return hits;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The identity a non-finding line keeps across a rewrite, per register. One implementation, because the
|
|
60
|
+
* observation below and the removal backstop in pipeline.mjs must agree on which row is which. PURE.
|
|
61
|
+
*/
|
|
62
|
+
export const REPORT_LINE_KEY = Object.freeze({
|
|
63
|
+
coverage: (c) => `coverage:${norm(c?.area)}`,
|
|
64
|
+
actions: (a) => `action:${a?.id ?? a?.kind ?? ""}`,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The reader's name for such a row. One implementation, because three surfaces print it: the plain-words
|
|
69
|
+
* pre-check that hands the reviewer its labels, the observation below, and the driver's own account of
|
|
70
|
+
* what it restored — and a reviewer comparing two of those must not be reading two vocabularies. PURE.
|
|
71
|
+
*/
|
|
72
|
+
export const REPORT_LINE_LABEL = Object.freeze({
|
|
73
|
+
coverage: (c) => `the coverage line for "${c?.area ?? "an area"}"`,
|
|
74
|
+
actions: (a) => `the action "${a?.id ?? a?.kind ?? ""}"`.replace(/ ""$/, ""),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* THE LINES A CLEARANCE READER MEETS FIRST THAT ARE NOT A FINDING — each coverage row's note, each
|
|
79
|
+
* action's text and the mark assessment's two reads — with the identity each keeps across a rewrite.
|
|
80
|
+
*
|
|
81
|
+
* A finding carries an ordinal and a name, and a flag joins to it by either. These lines carry neither,
|
|
82
|
+
* so a flag rewriting one came out `not-entity-scoped` whatever the corrective pass did with it: seven
|
|
83
|
+
* such flags survived one clearance on the test box, and a production matter showed the same split
|
|
84
|
+
* (2026-09-11). The label is the one the plain-words pre-check hands the reviewer, so a flag repeating it
|
|
85
|
+
* joins on the driver's own words.
|
|
86
|
+
*
|
|
87
|
+
* Reads the parsed record (`markAssessment`) and the raw one (`mark_assessment`) alike. PURE.
|
|
88
|
+
*/
|
|
89
|
+
export function reportLines(doc) {
|
|
90
|
+
const out = [];
|
|
91
|
+
const add = (key, label, v) => {
|
|
92
|
+
const text = typeof v === "string" ? v : (typeof v?.read === "string" ? v.read : "");
|
|
93
|
+
if (text.trim()) out.push({ key, label, text });
|
|
94
|
+
};
|
|
95
|
+
for (const c of Array.isArray(doc?.coverage) ? doc.coverage : [])
|
|
96
|
+
add(REPORT_LINE_KEY.coverage(c), REPORT_LINE_LABEL.coverage(c), c?.note);
|
|
97
|
+
for (const a of Array.isArray(doc?.actions) ? doc.actions : [])
|
|
98
|
+
add(REPORT_LINE_KEY.actions(a), REPORT_LINE_LABEL.actions(a), a?.text);
|
|
99
|
+
const ma = doc?.markAssessment ?? doc?.mark_assessment;
|
|
100
|
+
for (const k of ["distinctiveness", "connotation"]) add(`mark-assessment:${k}`, `the mark assessment's ${k}`, ma?.[k]);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const QUOTE_WORDS = 8;
|
|
105
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
|
+
|
|
107
|
+
/** A flag that carries the line's identity: its label, or the plain ways of saying it. PURE. */
|
|
108
|
+
function namesLine(hay, l) {
|
|
109
|
+
const at = l.key.indexOf(":");
|
|
110
|
+
const kind = l.key.slice(0, at), id = norm(l.key.slice(at + 1));
|
|
111
|
+
if (!id) return false;
|
|
112
|
+
if (kind === "coverage") return new RegExp(` coverage (?:line|note|row|entry) (?:for |on |about )?(?:the )?${escapeRe(id)} `).test(hay);
|
|
113
|
+
if (kind === "action") return hay.includes(` action ${id} `);
|
|
114
|
+
return new RegExp(` mark assessment (?:s )?(?:[a-z]+ )?${escapeRe(id)} `).test(hay);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A flag that quotes the line: eight running words of it, or all of a shorter one of four or more. PURE. */
|
|
118
|
+
function quotesLine(hay, text) {
|
|
119
|
+
const words = norm(text).split(" ").filter(Boolean);
|
|
120
|
+
if (words.length < QUOTE_WORDS) return words.length >= 4 && hay.includes(` ${words.join(" ")} `);
|
|
121
|
+
for (let i = 0; i + QUOTE_WORDS <= words.length; i++)
|
|
122
|
+
if (hay.includes(` ${words.slice(i, i + QUOTE_WORDS).join(" ")} `)) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Which of `lines` a flag is about — named by its label, or quoted, which is what the reviewer is told
|
|
128
|
+
* to do with a sentence it rewrites.
|
|
129
|
+
*
|
|
130
|
+
* THIS OBSERVES; IT APPLIES NOTHING. Matching prose is how a rewrite lands on the wrong thing, which is
|
|
131
|
+
* why no correction is ever routed by it. Here a miss leaves the row where it was, `not-entity-scoped`,
|
|
132
|
+
* and a hit prints the line's label beside the flag, where the reviewer reading the table can see it. PURE.
|
|
133
|
+
*/
|
|
134
|
+
export function linesOf(flagText, lines) {
|
|
135
|
+
const hay = ` ${norm(flagText)} `;
|
|
136
|
+
return (lines ?? []).filter((l) => namesLine(hay, l) || quotesLine(hay, l.text));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The corrective pass's own words about a line it was flagged on, from the `corrections` register. PURE. */
|
|
140
|
+
function reasonFor(hit, doc) {
|
|
141
|
+
const c = doc?.corrections;
|
|
142
|
+
const said = [
|
|
143
|
+
...(Array.isArray(c?.entries) ? c.entries.map((e) => [e?.entity, e?.disposition, e?.note].filter(Boolean).join(": ")) : []),
|
|
144
|
+
...String(c?.note ?? "").split(/\n|;\s+/),
|
|
145
|
+
].map((s) => s.trim()).filter(Boolean);
|
|
146
|
+
const hitSays = said.find((s) => linesOf(s, hit).length);
|
|
147
|
+
return hitSays ? hitSays.slice(0, 300) : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const squash = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
|
|
151
|
+
|
|
58
152
|
/** A finding's comparable state — the fields a correction can move. PURE. */
|
|
59
153
|
const stateOf = (f) => JSON.stringify({
|
|
60
154
|
disposition: f?.disposition ?? null,
|
|
@@ -82,7 +176,10 @@ function byName(doc) {
|
|
|
82
176
|
* findings-removed — a finding this flag names was present before the pass and is GONE after it;
|
|
83
177
|
* findings-changed — at least one finding this flag names has a different comparable state;
|
|
84
178
|
* findings-unchanged — it named findings and none of them moved;
|
|
85
|
-
*
|
|
179
|
+
* line-removed — a non-finding line this flag names or quotes (`reportLines`) is GONE after it;
|
|
180
|
+
* line-changed — that line's text is different after the pass;
|
|
181
|
+
* line-unchanged — it reads the same; the row carries the pass's reason, or null for none;
|
|
182
|
+
* not-entity-scoped — the flag names no finding and no such line (a prose/structure correction);
|
|
86
183
|
* not-checkable — the pre-corrective snapshot is missing, so nothing can be compared.
|
|
87
184
|
*
|
|
88
185
|
* "findings-unchanged" is NOT a failure and must never be rendered as one: correcting a narrative
|
|
@@ -162,6 +259,7 @@ export function buildCorrectionsApplied(rows, preDoc, postDoc) {
|
|
|
162
259
|
const known = knownEntities(preDoc, postDoc);
|
|
163
260
|
const pre = byName(preDoc), post = byName(postDoc);
|
|
164
261
|
const preOrd = byOrdinal(preDoc), postOrd = byOrdinal(postDoc);
|
|
262
|
+
const preLines = reportLines(preDoc), postLine = new Map(reportLines(postDoc).map((l) => [l.key, l]));
|
|
165
263
|
return (rows ?? []).map((r) => {
|
|
166
264
|
// — a DECLARED ordinal wins over the name match. `targetsOf` is a normalised prose join and it
|
|
167
265
|
// is why six of nine flags on a delivered run resolved to nothing; the declaration is the reviewer's
|
|
@@ -185,10 +283,22 @@ export function buildCorrectionsApplied(rows, preDoc, postDoc) {
|
|
|
185
283
|
const declaredOrds = (Array.isArray(r.ordinals) ? r.ordinals : [])
|
|
186
284
|
.filter((o) => preOrd.has(o) || postOrd.has(o));
|
|
187
285
|
const label = (o) => { const f = preOrd.get(o) ?? postOrd.get(o); return f?.mark ?? f?.owner?.name ?? `finding ${o}`; };
|
|
188
|
-
|
|
286
|
+
// — A LINE THAT IS NOT A FINDING, asked before the name match. A flag that names or quotes a coverage
|
|
287
|
+
// note, an action or the mark assessment is about that line, and a mark or owner named inside the
|
|
288
|
+
// line it quotes is incidental. Asked only where no ordinal was declared: a declaration still wins.
|
|
289
|
+
const hit = preDoc && !declaredOrds.length ? linesOf(r.text, preLines) : [];
|
|
290
|
+
const targets = declaredOrds.length ? declaredOrds.map(label)
|
|
291
|
+
: hit.length ? hit.map((l) => l.label) : targetsOf(r.text, known);
|
|
189
292
|
let outcome;
|
|
190
293
|
let removed = [];
|
|
191
294
|
if (!preDoc) outcome = "not-checkable";
|
|
295
|
+
else if (hit.length) {
|
|
296
|
+
// The same three answers a finding gets, asked of the line's own text. REMOVAL WINS, as it does
|
|
297
|
+
// for a finding: a line gone after the pass was answered by deletion, and that is the question.
|
|
298
|
+
const gone = hit.filter((l) => !postLine.has(l.key));
|
|
299
|
+
if (gone.length) { outcome = "line-removed"; removed = gone.map((l) => l.label); }
|
|
300
|
+
else outcome = hit.some((l) => squash(postLine.get(l.key).text) !== squash(l.text)) ? "line-changed" : "line-unchanged";
|
|
301
|
+
}
|
|
192
302
|
else if (declaredOrds.length) {
|
|
193
303
|
// The certain path. Every question below is asked of the finding the reviewer NAMED, by ordinal.
|
|
194
304
|
removed = declaredOrds.filter((o) => preOrd.has(o) && !postOrd.has(o)).map(label);
|
|
@@ -229,7 +339,10 @@ export function buildCorrectionsApplied(rows, preDoc, postDoc) {
|
|
|
229
339
|
// unrecoverable; one field makes it answerable from the next run on. It also feeds the report's
|
|
230
340
|
// open-points section, which prints `(finding N)` beside a point and had no ordinals to print.
|
|
231
341
|
return { n: r.n, kind: r.kind, typed: r.typed, text: r.text,
|
|
232
|
-
ordinals: Array.isArray(r.ordinals) ? r.ordinals : null, targets, outcome, removed
|
|
342
|
+
ordinals: Array.isArray(r.ordinals) ? r.ordinals : null, targets, outcome, removed,
|
|
343
|
+
// A line's row carries the pass's own reason for it, or null when the pass gave none: "declined
|
|
344
|
+
// with a reason" is then a fact the run recorded, and "neither applied nor declined" is too.
|
|
345
|
+
...(hit.length && preDoc ? { reason: reasonFor(hit, postDoc) } : {}) };
|
|
233
346
|
});
|
|
234
347
|
}
|
|
235
348
|
|
|
@@ -256,9 +369,12 @@ export function buildCorrectionsApplied(rows, preDoc, postDoc) {
|
|
|
256
369
|
* belongs on the printed side. Deciding it here rather than at the report keeps one implementation of a
|
|
257
370
|
* question this file already answered once.
|
|
258
371
|
*
|
|
372
|
+
* `line-changed` is the same fact about a line that is not a finding, so it is resolved too; its
|
|
373
|
+
* `line-removed` and `line-unchanged` stay printed for the reasons their finding twins do.
|
|
374
|
+
*
|
|
259
375
|
* PURE.
|
|
260
376
|
*/
|
|
261
|
-
export const RESOLVED_OUTCOMES = Object.freeze(["findings-changed"]);
|
|
377
|
+
export const RESOLVED_OUTCOMES = Object.freeze(["findings-changed", "line-changed"]);
|
|
262
378
|
|
|
263
379
|
/** The rows to print. Anything not positively resolved, including a row of an outcome nobody has met. */
|
|
264
380
|
export const unresolvedFlags = (rows) =>
|
|
@@ -307,8 +423,9 @@ export function correctionsAppliedTable(applied) {
|
|
|
307
423
|
for (const r of applied) {
|
|
308
424
|
// — a removal NAMES what left. `findings-removed` alone would tell the recheck that something
|
|
309
425
|
// was deleted and not which fact, which is the half it needs to decide whether the deletion was legitimate.
|
|
310
|
-
const what = r.outcome === "findings-removed" && r.removed?.length
|
|
426
|
+
const what = (r.outcome === "findings-removed" || r.outcome === "line-removed") && r.removed?.length
|
|
311
427
|
? `${r.outcome}: ${r.removed.join(", ")}`
|
|
428
|
+
: r.reason ? `${r.outcome} — the pass said: ${r.reason.slice(0, 120)}${r.reason.length > 120 ? "…" : ""}`
|
|
312
429
|
: r.outcome;
|
|
313
430
|
out.push(`| ${r.n} | ${r.kind} | ${r.text.slice(0, 90)}${r.text.length > 90 ? "…" : ""} | ${r.targets.join(", ") || "—"} | ${what} |`);
|
|
314
431
|
}
|
|
@@ -322,5 +439,11 @@ export function correctionsAppliedTable(applied) {
|
|
|
322
439
|
+ "so it reads as resolved while the report is no truer than before. Check each named finding: a "
|
|
323
440
|
+ "withdrawal the evidence supports is legitimate and belongs in the record AS a withdrawal with its "
|
|
324
441
|
+ "reason; a fact removed because it was inconvenient to correct is the defect this row exists to show you.");
|
|
442
|
+
if (applied.some((r) => String(r.outcome).startsWith("line-")))
|
|
443
|
+
out.push("",
|
|
444
|
+
"`line-*` rows compare a line the reader sees that is not a finding — a coverage note, an action, the mark "
|
|
445
|
+
+ "assessment — as the flag named or quoted it, before and after the pass. A `line-unchanged` row with no "
|
|
446
|
+
+ "reason beside it is a flag the pass neither applied nor declined; `line-removed` means the line is gone "
|
|
447
|
+
+ "rather than rewritten.");
|
|
325
448
|
return out.join("\n");
|
|
326
449
|
}
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
// stage apart. This module is the single answer. `cut/` cannot import it (that directory does not travel
|
|
24
24
|
// and this one does), so the pack gate restates the disjunction and its own test pins the two together.
|
|
25
25
|
|
|
26
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
28
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
29
29
|
|
|
30
30
|
/** The entry file each lane's publisher reads as its source, in the order a child is probed for one. */
|
|
31
31
|
export const ENTRY_FILES = Object.freeze(["report.md", "knockout-findings.json"]);
|
|
@@ -53,6 +53,49 @@ export function demoChildren(root) {
|
|
|
53
53
|
.filter((n) => isFrozen(join(root, n)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const why = (e) => e?.code ?? e?.message ?? String(e);
|
|
57
|
+
const NOT_FROZEN = "it holds no meta.json and lane entry file, so it is not a frozen demo";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* EVERY SAMPLE THE CONTAINER HOLDS, the ones that cannot be used NAMED rather than dropped.
|
|
61
|
+
*
|
|
62
|
+
* `demoChildren` answers "which can be replayed" and drops the rest, which is right for a caller choosing
|
|
63
|
+
* one and wrong for a caller replaying all of them. A sample whose directory could not be read vanished,
|
|
64
|
+
* and the demo said "3 demo reports are published and listed — one per product" over a package that ships
|
|
65
|
+
* four (measured on a published beta, 2026-09-11). Here every directory in the container counts: one that
|
|
66
|
+
* cannot be read, or is not a frozen demo, comes back in `unusable` with the reason.
|
|
67
|
+
*/
|
|
68
|
+
export function demoInventory(root) {
|
|
69
|
+
let entries;
|
|
70
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { return { children: [], unusable: [] }; }
|
|
71
|
+
const children = [], unusable = [];
|
|
72
|
+
for (const name of entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort()) {
|
|
73
|
+
const dir = join(root, name);
|
|
74
|
+
try { readdirSync(dir); } catch (e) { unusable.push({ name, why: `its directory could not be read (${why(e)})` }); continue; }
|
|
75
|
+
if (isFrozen(dir)) children.push(name);
|
|
76
|
+
else unusable.push({ name, why: NOT_FROZEN });
|
|
77
|
+
}
|
|
78
|
+
return { children, unusable };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ONE SAMPLE, READ AND COPIED TO PUBLISH FROM — or the reason it could not be. `{ sample }` or `{ name, why }`.
|
|
83
|
+
*
|
|
84
|
+
* A file inside a sample that could not be read made the copy throw, and the uncaught EACCES took all four
|
|
85
|
+
* demos down with a stack trace (measured on a published beta, 2026-09-11). It is returned instead, so the
|
|
86
|
+
* caller replays the others and names this one.
|
|
87
|
+
*/
|
|
88
|
+
export function prepareSample(dir, { repoRoot, tmp } = {}) {
|
|
89
|
+
const name = basename(dir);
|
|
90
|
+
if (!isFrozen(dir)) return { name, why: NOT_FROZEN };
|
|
91
|
+
const manifest = join(dir, "meta.json");
|
|
92
|
+
let meta;
|
|
93
|
+
try { meta = JSON.parse(readFileSync(manifest, "utf8")); } catch (e) { return { name, why: `its meta.json could not be read (${why(e)})` }; }
|
|
94
|
+
if (!meta?.runId) return { name, why: `${manifest} names no runId, so it is not a frozen demo manifest` };
|
|
95
|
+
try { return { sample: { dir, meta, name, publishFrom: publishSource(dir, { repoRoot, ...(tmp ? { tmp } : {}) }) } }; }
|
|
96
|
+
catch (e) { return { name, why: `it could not be copied to publish from (${why(e)})` }; }
|
|
97
|
+
}
|
|
98
|
+
|
|
56
99
|
|
|
57
100
|
/**
|
|
58
101
|
* The directory a frozen demo should be PUBLISHED from — itself, or a copy when it is part of this tree.
|
|
@@ -79,6 +122,33 @@ export function publishSource(dir, { repoRoot, tmp = tmpdir() } = {}) {
|
|
|
79
122
|
return copy;
|
|
80
123
|
}
|
|
81
124
|
|
|
125
|
+
/**
|
|
126
|
+
* THE WHOLE CONTAINER, PUBLISHED FROM — `publishSource`'s rule applied one sample at a time.
|
|
127
|
+
* `{ dir, unusable }`: the directory to publish the container from, and every sample left out, named.
|
|
128
|
+
*
|
|
129
|
+
* The launcher seeds the portal's archive from the whole container, and copied it in one call: one file
|
|
130
|
+
* it could not read failed the copy, and with it every sample, so the archive came up empty over three
|
|
131
|
+
* good ones (driven on a published beta's container, 2026-09-11). Here a sample that cannot be copied is
|
|
132
|
+
* left out and named, and the others are published.
|
|
133
|
+
*/
|
|
134
|
+
export function publishContainer(root, { repoRoot, tmp = tmpdir() } = {}) {
|
|
135
|
+
const repo = resolve(repoRoot ?? "");
|
|
136
|
+
const here = resolve(root);
|
|
137
|
+
if (!repo || !(here === repo || here.startsWith(repo + sep))) return { dir: root, unusable: demoInventory(root).unusable };
|
|
138
|
+
const copy = join(mkdtempSync(join(tmp, "clearotron-demo-")), "sample");
|
|
139
|
+
mkdirSync(copy, { recursive: true });
|
|
140
|
+
const { children, unusable } = demoInventory(here);
|
|
141
|
+
const left = [...unusable];
|
|
142
|
+
for (const name of children) {
|
|
143
|
+
try { cpSync(join(here, name), join(copy, name), { recursive: true }); }
|
|
144
|
+
catch (e) {
|
|
145
|
+
rmSync(join(copy, name), { recursive: true, force: true });
|
|
146
|
+
left.push({ name, why: `it could not be copied to publish from (${why(e)})` });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { dir: copy, unusable: left };
|
|
150
|
+
}
|
|
151
|
+
|
|
82
152
|
/**
|
|
83
153
|
* THE DEMO'S SAMPLE RUNS, WHERE AN ASSISTANT LOOKS FOR RUNS.
|
|
84
154
|
*
|
|
@@ -100,7 +170,7 @@ export function publishSource(dir, { repoRoot, tmp = tmpdir() } = {}) {
|
|
|
100
170
|
* portal, and a copy laid down by an earlier version still carries the old host.
|
|
101
171
|
*/
|
|
102
172
|
export function seedDemoRuns({ workspace, examplesDir, portalOrigin = null }) {
|
|
103
|
-
const seeded = [], already = [];
|
|
173
|
+
const seeded = [], already = [], failed = [];
|
|
104
174
|
for (const name of demoChildren(examplesDir)) {
|
|
105
175
|
const run = join(examplesDir, name, "run");
|
|
106
176
|
let s;
|
|
@@ -109,13 +179,20 @@ export function seedDemoRuns({ workspace, examplesDir, portalOrigin = null }) {
|
|
|
109
179
|
const dir = join(workspace, `workspace-${s.agent || "clawdi"}`, "studio", "prelim-search", s.slug, `${s.date}-${s.codename}`);
|
|
110
180
|
if (existsSync(join(dir, "status.json"))) already.push(s.runId);
|
|
111
181
|
else {
|
|
112
|
-
|
|
113
|
-
|
|
182
|
+
// ONE SAMPLE'S UNREADABLE FILE COSTS THAT SAMPLE ONLY, and is named, never a throw out of the loop.
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
185
|
+
cpSync(run, dir, { recursive: true });
|
|
186
|
+
} catch (e) {
|
|
187
|
+
rmSync(dir, { recursive: true, force: true });
|
|
188
|
+
failed.push({ name, why: `its run could not be copied (${why(e)})` });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
114
191
|
seeded.push(s.runId);
|
|
115
192
|
}
|
|
116
193
|
if (portalOrigin) stampReportLinks(join(dir, "status.json"), portalOrigin);
|
|
117
194
|
}
|
|
118
|
-
return { seeded, already };
|
|
195
|
+
return { seeded, already, failed };
|
|
119
196
|
}
|
|
120
197
|
|
|
121
198
|
/** The portal route that serves a run's report: the one `scanAccountRuns` hands the portal's own list. */
|
package/driver/gateway.mjs
CHANGED
|
@@ -816,7 +816,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
816
816
|
followup = false, // #5b: this run is a warm-resume / followup (escalation, envelope close, frame-reopen
|
|
817
817
|
// sweep) — a hard-wall timeout breaks after ONE attempt (a 1.5× extension can't fit
|
|
818
818
|
// an already-over-budget resume; the caller records the coverage-limited deferral).
|
|
819
|
-
excludeTools,
|
|
819
|
+
excludeTools, bandSize, // copper-lattice re-route: tool names dropped from this stage's allowedTools
|
|
820
820
|
} = opts;
|
|
821
821
|
if (!message) throw new Error(`runStage(${name}): message is required`);
|
|
822
822
|
if (!sessionKey) throw new Error(`runStage(${name}): sessionKey is required`);
|
|
@@ -1741,7 +1741,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1741
1741
|
//
|
|
1742
1742
|
// null (not 0, and not absent) on an engine that cannot report them, so "this adapter does not
|
|
1743
1743
|
// measure" stays visibly different from "this turn called no tools" — see toolGauge.
|
|
1744
|
-
...toolGauge(turn),
|
|
1744
|
+
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1745
1745
|
// AD-4 emitted-vs-landed, UNCONDITIONAL (was success-only, which made a failed attempt's mid-write
|
|
1746
1746
|
// artifact invisible): `output` = what LANDED on disk after this attempt (null when the stage has no
|
|
1747
1747
|
// expected file); `wrote` = whether THIS attempt emitted it (see the computation above the runDir
|
|
@@ -1804,7 +1804,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1804
1804
|
// 485-second killed dispatch is exactly the one a 0 here would erase.
|
|
1805
1805
|
wall, outputTokens: usage?.output ?? null, tokensPerSec: tokensPerSec(usage, wall),
|
|
1806
1806
|
// — the spine carries them too, or a round has to join two files to ask why a stage was slow.
|
|
1807
|
-
...toolGauge(turn),
|
|
1807
|
+
...toolGauge(turn), band: bandSize ?? undefined,
|
|
1808
1808
|
formRepairs: formRepairsThisAttempt || undefined, //, see the stage row above
|
|
1809
1809
|
});
|
|
1810
1810
|
} catch { /* telemetry best-effort — never fail a turn over a journal line */ }
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0",
|
|
5
|
+
"version": "0.3.1-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|