clearotron 0.2.1 → 0.3.0-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.
Files changed (45) hide show
  1. package/INSTALL.md +5 -4
  2. package/bin/example.mjs +9 -5
  3. package/bin/onboard.mjs +19 -19
  4. package/bin/stop.mjs +65 -3
  5. package/build-info.json +2 -2
  6. package/driver/CHANGELOG.md +21 -0
  7. package/driver/declination-call.mjs +32 -0
  8. package/driver/driver.config.mjs +20 -0
  9. package/driver/engine/mcp/recording-server.mjs +4 -0
  10. package/driver/gateway.mjs +8 -3
  11. package/driver/knockout-assess-record.mjs +5 -1
  12. package/driver/package.json +1 -1
  13. package/driver/pipeline.mjs +83 -2
  14. package/driver/predelivery-lint.mjs +22 -4
  15. package/driver/publish/knockout.mjs +12 -6
  16. package/driver/publish/render-knockout.mjs +133 -23
  17. package/driver/publish/report-data.mjs +13 -3
  18. package/driver/record-carry.mjs +2 -2
  19. package/driver/reference-score.mjs +1 -1
  20. package/driver/result-noun-fields.mjs +7 -0
  21. package/driver/skills/knockout-assess/SKILL.md +10 -4
  22. package/driver/stages-knockout.mjs +1 -1
  23. package/driver/stages.mjs +1 -1
  24. package/driver/suite-census.json +66 -18
  25. package/driver/unit-inventory.mjs +47 -0
  26. package/driver/unit-state-verdict.mjs +8 -8
  27. package/driver/verify-knockout.mjs +9 -1
  28. package/driver/whatif-memo-run.mjs +45 -4
  29. package/mcp-server/CHANGELOG.md +2 -0
  30. package/mcp-server/lib/brief.mjs +15 -0
  31. package/mcp-server/lib/driver.mjs +6 -0
  32. package/mcp-server/lib/knockout.mjs +435 -0
  33. package/mcp-server/lib/scrub.mjs +1 -1
  34. package/mcp-server/package.json +1 -1
  35. package/mcp-server/server.mjs +69 -4
  36. package/package.json +1 -1
  37. package/portal-ui/package.json +1 -1
  38. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  39. package/providers/oauth-mcp-bridge/package.json +1 -1
  40. package/scripts/drain-preflight.mjs +2 -2
  41. package/scripts/freeze-example-run.mjs +3 -3
  42. package/scripts/headless-page.mjs +51 -2
  43. package/scripts/live-surface-check.mjs +86 -17
  44. package/scripts/render-check.mjs +61 -2
  45. package/scripts/deploy-test.sh +0 -309
package/INSTALL.md CHANGED
@@ -41,10 +41,11 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
41
41
  - **Node.js >= 22**, and npm. A hard floor: `package.json` declares it, `.nvmrc` pins it, and the free
42
42
  US register runs on `node:sqlite`, which ships with FTS5 from Node 22. Node 20 installs and then fails
43
43
  at the first US search. `nvm use` picks the pin up.
44
- - **macOS, Linux, or Windows via WSL2.** Native Windows is not supported: the engine resolves the
45
- reasoning CLI the POSIX way, so a native-Windows run refuses at preflight even with the CLI on `PATH`.
46
- On Windows, `wsl --install`, then install Node 22 **inside** the Linux distribution and work through
47
- this page from there. `npx clearotron demo` is the exception and runs anywhere Node does. A *hosted*
44
+ - **macOS, Linux, or native Windows for the demo; WSL2 with Node 22 for a clearance.** `npx clearotron
45
+ demo` runs anywhere Node does, native Windows included. A real clearance does not: the engine resolves
46
+ the reasoning CLI the POSIX way, so a native-Windows clearance refuses at preflight even with the CLI
47
+ on `PATH`. On Windows, `wsl --install`, then install Node 22 **inside** the Linux distribution and work
48
+ through this page from there. A *hosted*
48
49
  deployment needs Linux for one further thing, the systemd outbox trigger —
49
50
  [driver/systemd/README.md](driver/systemd/README.md).
50
51
  - **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
package/bin/example.mjs CHANGED
@@ -41,7 +41,7 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync,
41
41
  import { homedir, tmpdir } from "node:os";
42
42
  import { invoke } from "../shared/invocation.mjs"; // — the printed command is resolved once, for the reader who is actually standing there
43
43
  import { basename, dirname, join, resolve, sep } from "node:path";
44
- import { fileURLToPath } from "node:url";
44
+ import { fileURLToPath, pathToFileURL } from "node:url";
45
45
  import { spawn } from "node:child_process";
46
46
  import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
47
47
  import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
@@ -68,7 +68,11 @@ if (has("--help") || has("-h")) {
68
68
  const shipped = demoChildren(join(REPO, "demo"));
69
69
  if (shipped.length) {
70
70
  console.log(`\n --product takes one of:`);
71
- for (const id of shipped) console.log(` ${id}${id === shipped[0] ? " (the default, when --product is not given)" : ""}`);
71
+ // NO "DEFAULT" MARKER (tracker issue 277). This printed "(the default, when --product is not given)"
72
+ // beside the first id, and kept printing it after the default became ALL of them — so `--help` taught
73
+ // a reader the exact belief the change removed, in the command it was reported against.
74
+ for (const id of shipped) console.log(` ${id}`);
75
+ console.log(`\n With no --product, every one of them is published.`);
72
76
  console.log("");
73
77
  } else {
74
78
  // An absence is a finding: a tree with no demo says so rather than printing an empty list.
@@ -96,8 +100,8 @@ const DEMO_ROOT = join(REPO, "demo");
96
100
  // disagreed. That file records what a knockout demo carries instead of a report.md, and why this line
97
101
  // once let `demo/knockout-search` ship and stay unopenable..
98
102
 
99
- // --run-dir takes a directory outright. --product names a child. Neither given: the first child, and the
100
- // name is PRINTED below rather than assumed, because "the demo" is about to mean one of several.
103
+ // --run-dir takes a directory outright. --product names a child. Neither given: EVERY child, and the
104
+ // names are PRINTED below rather than assumed, because "the demo" means several.
101
105
  const wanted = flag("--product");
102
106
  const children = demoChildren(DEMO_ROOT);
103
107
 
@@ -220,7 +224,7 @@ console.log(samples.length === 1
220
224
  console.log(` pool: ${poolRoot}\n`);
221
225
 
222
226
  mkdirSync(poolRoot, { recursive: true });
223
- const { republishRun } = await import(join(REPO, "driver", "publish", "report-registry.mjs"));
227
+ const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publish", "report-registry.mjs")).href);
224
228
 
225
229
  // ── ONE FAILURE MUST NOT COST THE OTHERS, AND MUST NOT BE A QUIET COUNT (tracker issue 277) ─────────
226
230
  //
package/bin/onboard.mjs CHANGED
@@ -693,8 +693,8 @@ export async function preflightCandidate(candidate) {
693
693
  /** Ask EUIPO for a token. This is the check that makes "refuses to persist a bad secret" true. */
694
694
  export async function validateEuipo({ clientId, clientSecret, environment }) {
695
695
  try {
696
- const { resolveConfig } = await import(join(REPO, "providers", "euipo", "src", "core.js"));
697
- const { getAccessToken } = await import(join(REPO, "providers", "euipo", "src", "euipo-client.js"));
696
+ const { resolveConfig } = await import(pathToFileURL(join(REPO, "providers", "euipo", "src", "core.js")).href);
697
+ const { getAccessToken } = await import(pathToFileURL(join(REPO, "providers", "euipo", "src", "euipo-client.js")).href);
698
698
  await getAccessToken(resolveConfig({ clientId, clientSecret, environment }), { force: true });
699
699
  return { ok: true };
700
700
  } catch (e) {
@@ -705,7 +705,7 @@ export async function validateEuipo({ clientId, clientSecret, environment }) {
705
705
  /** One minimal Perplexity call. Costs a request, so it is never made without being asked for. */
706
706
  export async function validatePerplexity(apiKey) {
707
707
  try {
708
- const { buildRequestBody, callAgentAPI } = await import(join(REPO, "providers", "perplexity", "src", "core.js"));
708
+ const { buildRequestBody, callAgentAPI } = await import(pathToFileURL(join(REPO, "providers", "perplexity", "src", "core.js")).href);
709
709
  await callAgentAPI(apiKey, buildRequestBody({ task: "Reply with the single word: ok.", preset: "fast-search" }), { retries: 0 });
710
710
  return { ok: true };
711
711
  } catch (e) {
@@ -1053,7 +1053,7 @@ export async function runCheck() {
1053
1053
  // deliberate — a second reader would drift from this one exactly as the composer and the checker did
1054
1054
  // in F41, and the drift is invisible because both sides keep passing their own arms.
1055
1055
  const unitDir = join(homedir(), ".config", "systemd", "user");
1056
- const { BACKGROUND_UNITS } = await import(join(REPO, "bin", "start.mjs"));
1056
+ const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1057
1057
  const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
1058
1058
  const unitEnv = hosted
1059
1059
  ? unitEnvironment({
@@ -1908,8 +1908,8 @@ export async function runCheck() {
1908
1908
  say("\n Register lane — proven, not inferred");
1909
1909
  try {
1910
1910
  const [{ activeProvider }, { makeLaneProbe, probeSpend, DEFAULT_CONTROLS, loadProviderCapabilities }] = await Promise.all([
1911
- import(join(REPO, "driver", "driver.config.mjs")),
1912
- import(join(REPO, "providers", "_shared", "lane-probe.mjs")),
1911
+ import(pathToFileURL(join(REPO, "driver", "driver.config.mjs")).href),
1912
+ import(pathToFileURL(join(REPO, "providers", "_shared", "lane-probe.mjs")).href),
1913
1913
  ]);
1914
1914
  const adapter = activeProvider();
1915
1915
  // NOT `adapter.capabilities` — that is null on every adapter, and reading it announced a LOCAL
@@ -1995,7 +1995,7 @@ export async function runCheck() {
1995
1995
  // green over that state would bless the owner's fresh-install 500.
1996
1996
  say("\n Profile store");
1997
1997
  try {
1998
- const { loadProfiles } = await import(join(REPO, "driver", "profiles.mjs"));
1998
+ const { loadProfiles } = await import(pathToFileURL(join(REPO, "driver", "profiles.mjs")).href);
1999
1999
  const resolved = loadProfiles({ force: true });
2000
2000
  const named = [...resolved.keys()];
2001
2001
  ok(`resolves: ${named.length} profile(s) (${named.slice(0, 6).join(", ")}${named.length > 6 ? ", …" : ""}) — the universal fallback is present`);
@@ -2024,7 +2024,7 @@ export async function runCheck() {
2024
2024
  // an absence of evidence is not evidence of absence, and this whole check exists because something
2025
2025
  // invisible was being read as fine.
2026
2026
  {
2027
- const { retiredSpellingsIn, retiredSpellingLine } = await import(join(REPO, "shared", "env-aliases.mjs"));
2027
+ const { retiredSpellingsIn, retiredSpellingLine } = await import(pathToFileURL(join(REPO, "shared", "env-aliases.mjs")).href);
2028
2028
  const sources = [
2029
2029
  { label: "your environment file", env: fileEnv, known: true },
2030
2030
  { label: "this shell's environment", env: process.env, known: true },
@@ -2077,7 +2077,7 @@ export async function runCheck() {
2077
2077
 
2078
2078
  say("\n Submit lane");
2079
2079
  try {
2080
- const { triggerLaneVerdict, HOSTED, SUPERVISED } = await import(join(REPO, "shared", "trigger-lane.mjs"));
2080
+ const { triggerLaneVerdict, HOSTED, SUPERVISED } = await import(pathToFileURL(join(REPO, "shared", "trigger-lane.mjs")).href);
2081
2081
  // POSTURE FROM THE BOX, NOT FROM A FLAG. Units installed means the units serve, and the units read
2082
2082
  // %h/.env — so an absent value there is the incident. No units means `start` supervises and derives
2083
2083
  // the value at runtime, where reading this process's environment says nothing either way.
@@ -2117,7 +2117,7 @@ export async function runCheck() {
2117
2117
  // Stop control's availability on — one reader, two surfaces.
2118
2118
  let verbs = null;
2119
2119
  try {
2120
- const { opsTokenPosture } = await import(join(REPO, "driver", "portal-service.mjs"));
2120
+ const { opsTokenPosture } = await import(pathToFileURL(join(REPO, "driver", "portal-service.mjs")).href);
2121
2121
  verbs = opsTokenPosture(opsToken).verbs;
2122
2122
  } catch { /* unreadable posture leaves verbs null, which reads as full ops — see the verdict */ }
2123
2123
  let probe = null;
@@ -2158,7 +2158,7 @@ export async function runCheck() {
2158
2158
  // measured on a default install, a revoked key completed a full handshake with nothing logged. Nothing
2159
2159
  // surfaced that state anywhere, which is why it survived to be found by hand.
2160
2160
  {
2161
- const { defaultDenylistPath } = await import(join(REPO, "shared", "client-door.mjs"));
2161
+ const { defaultDenylistPath } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2162
2162
  const dl = effective("TRADEMARK_MCP_TOKEN_DENYLIST")?.v || defaultDenylistPath(homedir());
2163
2163
  try {
2164
2164
  const n = readFileSync(dl, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).length;
@@ -2172,8 +2172,8 @@ export async function runCheck() {
2172
2172
  }
2173
2173
  }
2174
2174
  try {
2175
- const { clientDoorState: doorState, describeDoorState, connectKeyReport, CLIENT_DOOR_UNIT: doorUnit } = await import(join(REPO, "shared", "client-door.mjs"));
2176
- const { loadGrants: readGrantsFile, isRevoked: revokedCheck } = await import(join(REPO, "shared", "scope.mjs"));
2175
+ const { clientDoorState: doorState, describeDoorState, connectKeyReport, CLIENT_DOOR_UNIT: doorUnit } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2176
+ const { loadGrants: readGrantsFile, isRevoked: revokedCheck } = await import(pathToFileURL(join(REPO, "shared", "scope.mjs")).href);
2177
2177
  // ── CONFIGURED IS NOT RUNNING, AND THIS IS THE SURFACE IT WAS MEASURED ON ──
2178
2178
  //
2179
2179
  // `describeDoorState` was written for exactly this block and then never called from it: the split
@@ -2249,7 +2249,7 @@ export async function runCheck() {
2249
2249
  // names the old one. A door that is up, listening, and refuses everything is the worst of the three
2250
2250
  // states to debug, and nothing surfaced the mismatch.
2251
2251
  {
2252
- const { clientDoorPort: portOf, allowedHosts: hostsFor } = await import(join(REPO, "shared", "client-door.mjs"));
2252
+ const { clientDoorPort: portOf, allowedHosts: hostsFor } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2253
2253
  const declared = String(effective("CLIENT_MCP_ALLOWED_HOSTS")?.v ?? "").trim();
2254
2254
  if (declared) {
2255
2255
  const port = portOf(doorEnv);
@@ -2272,7 +2272,7 @@ export async function runCheck() {
2272
2272
  // null — a probe that could not be made is not a door that is absent.
2273
2273
  let doorListening = null;
2274
2274
  try {
2275
- const { clientDoorPort: portOf } = await import(join(REPO, "shared", "client-door.mjs"));
2275
+ const { clientDoorPort: portOf } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2276
2276
  const { createConnection } = await import("node:net");
2277
2277
  doorListening = await new Promise((resolve) => {
2278
2278
  const sock = createConnection({ host: "127.0.0.1", port: portOf(doorEnv), timeout: 700 });
@@ -2310,7 +2310,7 @@ export async function runCheck() {
2310
2310
  // THE PUBLISHED ADDRESS, AND WHETHER IT ANSWERS (, acceptance 2). Reported here
2311
2311
  // rather than beside the unit, because the unit running and the address being reachable are
2312
2312
  // different facts and the second is the one a client depends on.
2313
- const { clientDoorReachability } = await import(join(REPO, "shared", "client-door.mjs"));
2313
+ const { clientDoorReachability } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
2314
2314
  // FROM THE FILE THE UNITS LOAD, NOT THE SHELL THIS COMMAND WAS TYPED IN (the tracker issue 226
2315
2315
  // family, met again here). On a hosted box the published address lives in the units' environment,
2316
2316
  // and reading `process.env` reported "no client connector address is published — that is correct
@@ -2357,7 +2357,7 @@ export async function runCheck() {
2357
2357
  // No new credential and no provider API: the audience is in the redirect handed to a caller with
2358
2358
  // no session at all, which is the request just made.
2359
2359
  {
2360
- const { readAudience, audienceVerdict } = await import(join(REPO, "shared", "access-audience.mjs"));
2360
+ const { readAudience, audienceVerdict } = await import(pathToFileURL(join(REPO, "shared", "access-audience.mjs")).href);
2361
2361
  const configuredAud = effectiveForService("CLEAROTRON_OIDC_AUDIENCE")?.v ?? "";
2362
2362
  const read = readAudience(probe ?? { error: "the published address was never asked" });
2363
2363
  // NOTHING IS CLAIMED ABOUT A BOX THAT USES NO EDGE. With no audience configured AND no Access
@@ -2383,7 +2383,7 @@ export async function runCheck() {
2383
2383
  // announces the side effect first, and its control runs before any vendor.
2384
2384
  if (PROBE_CONNECTOR && String(published ?? "").trim()) {
2385
2385
  const { registrationEndpointFrom, probeRegistration, describeRegistration } =
2386
- await import(join(REPO, "shared", "connector-signin-probe.mjs"));
2386
+ await import(pathToFileURL(join(REPO, "shared", "connector-signin-probe.mjs")).href);
2387
2387
  say("");
2388
2388
  say(" --probe-connector: asking this door's sign-in whether each assistant vendor could register.");
2389
2389
  say(" Each attempt that SUCCEEDS creates a throwaway OAuth client on your account, which you may");
@@ -3134,7 +3134,7 @@ try {
3134
3134
  prose("No default — it is your hostname. Skip it and this stays a local install: everything works",
3135
3135
  "here, and the Use-your-AI page says so rather than handing out an address that fails.");
3136
3136
  prose("The usual shape, and why: mcp-server/CONNECT.md.");
3137
- const { clientDoorReachability } = await import(join(REPO, "shared", "client-door.mjs"));
3137
+ const { clientDoorReachability } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
3138
3138
  for (;;) {
3139
3139
  const clientUrl = await askValue("Public connector address:", {
3140
3140
  skippable: true,
package/bin/stop.mjs CHANGED
@@ -19,12 +19,16 @@ import { existsSync, rmSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { homedir } from "node:os";
21
21
  import { execFileSync } from "node:child_process";
22
+ import { systemdSaid, looksLikeBusFailure, busRemedy, CAPTURE_STDERR } from "../shared/systemd-failure.mjs"; // tracker issue 270 — a stop that could not look must not report that it stopped
22
23
  import { BACKGROUND_UNITS } from "./start.mjs";
23
24
  import { CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
24
25
  import { invoke } from "../shared/invocation.mjs";
25
26
 
26
27
  const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
27
28
  const say = (s = "") => console.log(s);
29
+ // ON STDERR, because a refusal that scrolls past in the same stream as the success lines is a
30
+ // refusal a script cannot act on and a reader skims (tracker issue 270).
31
+ const err = (s = "") => console.error(s);
28
32
 
29
33
  const argv = process.argv.slice(2);
30
34
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -39,20 +43,76 @@ if (argv.includes("--help") || argv.includes("-h")) {
39
43
  }
40
44
 
41
45
  let found = 0;
46
+ const failures = [];
42
47
  for (const u of BACKGROUND_UNITS) {
43
48
  if (u === CLIENT_DOOR_UNIT) continue; // structurally unreachable (the pin's census forbids it); belt anyway
44
49
  const file = join(UNIT_DIR, u);
45
50
  if (!existsSync(file)) continue;
46
51
  found++;
47
- try { execFileSync("systemctl", ["--user", "disable", "--now", u], { stdio: "ignore" }); } catch { /* already down */ }
52
+ // ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED (tracker issue 270) ────────────────
53
+ //
54
+ // This was `catch { /* already down */ }` — a COMMENT standing in for a check. The comment guessed why
55
+ // the call failed, the next line deleted the unit file regardless, and the line after that announced
56
+ // "stopped and removed". Measured on a test box: `disable --now` failed for want of a session bus, all
57
+ // four services stayed up on unchanged pids holding all three ports, three unit files were deleted, and
58
+ // the command exited 0 saying the box ran nothing. The services were then unmanageable — running, with
59
+ // no unit file to stop them by — which is strictly worse than leaving both alone.
60
+ //
61
+ // `is-active` is what decides, not the exit code of the disable, because the exit code is what lied.
62
+ let stopped = false;
63
+ let why = null;
64
+ try {
65
+ execFileSync("systemctl", ["--user", "disable", "--now", u], CAPTURE_STDERR);
66
+ } catch (e) {
67
+ why = systemdSaid(e);
68
+ }
69
+ // ASKED AFTER, WHATEVER THE DISABLE SAID. A disable that reported failure may still have stopped it,
70
+ // and one that reported success may not have — only the state answers.
71
+ try {
72
+ const state = execFileSync("systemctl", ["--user", "is-active", u], { ...CAPTURE_STDERR, stdio: ["ignore", "pipe", "pipe"] });
73
+ stopped = String(state).trim() !== "active";
74
+ } catch (e) {
75
+ // `is-active` exits non-zero for an inactive unit, which is the answer we want — but it exits
76
+ // non-zero for "cannot reach systemd" too, and those must not read the same. The stdout is the
77
+ // discriminator: an unreachable systemd prints nothing there.
78
+ const said = String(e?.stdout ?? "").trim();
79
+ if (said && said !== "active") stopped = true;
80
+ else why = why ?? systemdSaid(e);
81
+ }
82
+
83
+ if (!stopped) {
84
+ // NOT DELETED. A running service with no unit file cannot be stopped by any ordinary means.
85
+ failures.push({ unit: u, why: why ?? "systemd still reports it active" });
86
+ say(` COULD NOT STOP ${u} — its unit file is left in place, so it can still be stopped`);
87
+ continue;
88
+ }
48
89
  try { rmSync(file, { force: true }); } catch { /* already gone */ }
49
90
  say(` stopped and removed ${u}`);
50
91
  }
51
- try { execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" }); } catch { /* no user bus */ }
92
+ // THE COMMENT HERE ALREADY NAMED THE CAUSE AND SHRUGGED AT IT. If there is no user bus, the disables
93
+ // above did not happen either — which is the whole of tracker issue 270 — so this is where that is said.
94
+ try {
95
+ execFileSync("systemctl", ["--user", "daemon-reload"], CAPTURE_STDERR);
96
+ } catch (e) {
97
+ const said = systemdSaid(e);
98
+ err(` could not ask systemd to reload its units — ${said}`);
99
+ if (looksLikeBusFailure(said)) err(`\n${busRemedy()}\n`);
100
+ }
52
101
 
53
102
  if (!found) {
54
103
  say(" Nothing was running in the background — no pinned unit is installed on this box.");
55
104
  say(" Nothing to do, and nothing was changed.");
105
+ } else if (failures.length) {
106
+ // THE SENTENCE THAT WAS WRONG. "The background product is stopped and the box runs nothing again" was
107
+ // printed unconditionally — including on the run where four services stayed up. A reader who is told
108
+ // that has no reason to look, which is what made the state unmanageable rather than merely wrong.
109
+ err("");
110
+ err(` ${failures.length} of ${found} service(s) could NOT be stopped, and their unit files are left in place:`);
111
+ for (const f of failures) err(` ${f.unit}: ${f.why}`);
112
+ if (failures.some((f) => looksLikeBusFailure(f.why))) err(`\n${busRemedy()}\n`);
113
+ err(" Nothing was removed for these, so they can still be stopped once systemd can be reached.");
114
+ err(" The box is NOT idle. This exits non-zero.");
115
+ process.exitCode = 1;
56
116
  } else {
57
117
  say("");
58
118
  say(" The background product is stopped and the box runs nothing again — plain `clearotron start`");
@@ -60,4 +120,6 @@ if (!found) {
60
120
  const door = existsSync(join(UNIT_DIR, CLIENT_DOOR_UNIT));
61
121
  if (door) say(` Your assistant connection is untouched and still up; \`${invoke("disconnect")}\` is what closes it and revokes its key.`);
62
122
  }
63
- process.exit(0);
123
+ // NOT A BARE ZERO. A stop that could not stop something sets `exitCode` above, and exiting 0 here would
124
+ // discard it — printing the refusal and then reporting success, which is the shape this change removes.
125
+ process.exit(process.exitCode ?? 0);
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "df42f25966148479a5a6d34aba3f82c110a1ba43",
3
- "version": "0.2.1"
2
+ "commit": "061581a9789002849189ef4e55aaa74289409d13",
3
+ "version": "0.3.0-beta.0"
4
4
  }
@@ -1,5 +1,26 @@
1
1
  # prelim-driver
2
2
 
3
+ ## 0.3.0-beta.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb1a409: New: A Knockout report now carries the whole assessment behind its ratings. You get the reviewer's notes and the reviewer's own opening read of each name. You also get the reasoning that holds a name at its rating, and what would move it. This was written during every search and reached only the audit workbook, so the report showed a rating without the thinking under it. The notes are marked as reference material rather than mixed into the findings.
8
+
9
+ New: A registered filing the search formed a view on now shows that rating and the read behind it. Before, the one registered right on a page was the only entry carrying no rating, beside softer uses that all carried one. That inverted what matters legally. A filing the search did not weigh still says so plainly.
10
+ - bb1a409: New: The tools that show how a search reached its answer now work on a Knockout search. You can ask what it found, what it looked at, where it searched and came back empty, and read the delivered report itself. Until now they returned nothing at all for a Knockout search, so anyone asking how one of these results was reached saw a blank record.
11
+
12
+ New: The proof-of-search record answers on these searches. It lists what was searched and found nothing, which is what answers a challenge to a result. Where a tool has nothing to show for this kind of search, it now says so in words. An empty list reads as "we looked and found nothing".
13
+
14
+ ### Patch Changes
15
+
16
+ - df6277c: Fixed: A live registration covering the goods you asked about is no longer left out of a report for want of room. It is reported, or the report says why it is distant.
17
+ - 9ba8cd0: Fixed: A what-if memo is now refused if it writes anywhere in the delivered run except its own memo folder, and the refusal names the file.
18
+ - 8c8e200: Fixed: A what-if question left queued when its run is archived now comes back with a reason, instead of never being answered.
19
+ - bb1a409: Fixed: The delivery record now states plainly when a report's write-up length and ranking rules could not be checked against the delivered text. It says the rules were applied to nothing on that run. Before, this was recorded as an unlabelled failed check. It looked like any other, so a run could deliver with those rules unverified and nobody would see it.
20
+ - 9ba8cd0: For operators: Each run record now says whether the model id it observed was a pinned snapshot or an alias the provider can repoint.
21
+ - aea4a4e: Fixed: The deployment check now says which checkout a service is running from even when its unit file does not declare one.
22
+ - 14d6d28: Fixed: The demo and the install steps now start on native Windows, where they previously crashed on the first internal module they loaded.
23
+
3
24
  ## 0.2.1
4
25
 
5
26
  ### Patch Changes
@@ -210,6 +210,38 @@ export function contradictionFor(reason, row, scope) {
210
210
  + `own-right or duplicate-of-delivered.`;
211
211
  }
212
212
 
213
+ // ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (owner ruling 2026-09-07) ─────────────────
214
+ //
215
+ // `not-worth-the-line` means "it fits none of the four manageable categories and is not relevant
216
+ // enough to drive the read". That is a statement about the REPORT'S BUDGET, not about the record. On a
217
+ // live registration inside one of the matter's own instructed classes it is the one thing the seat may
218
+ // not say: the reader is paying for a judgment about that mark, and "there was no room" is not one.
219
+ //
220
+ // Measured on the run that prompted the ruling: 26 of 49 declinations at this seam were this token,
221
+ // and two of the lawyer's own reference marks were among them — one carried on every earlier run.
222
+ //
223
+ // ✕ WHY THIS IS NOT THE REFUSAL THAT WAS REMOVED BELOW, and the difference is the whole reason it can
224
+ // stand. That one refused `unrelated-goods` and `off-field-not-major` on class overlap, fired on 170
225
+ // of 170 records, and made the ENGINE decide relatedness — the lawyer's call. This one refuses ONE
226
+ // token and leaves every substantive reason available. The seat may still decline this record: it may
227
+ // say the goods are unrelated, that it is off-field and not a major brand, that it is the applicant's
228
+ // own right, or that it is already delivered. What it may no longer do is decline it for taking up
229
+ // space. The refusal is about the SHAPE of the reason, not about whether the record is relevant, so
230
+ // the relatedness judgment stays exactly where it was.
231
+ //
232
+ // It does effectively retire this token for live in-class records, because the sweep is already scoped
233
+ // to the instructed classes. That was put to the owner as the consequence and ruled anyway.
234
+ if (reason === "not-worth-the-line" && isLive(row?.status) && shared.length) {
235
+ return `this record is ${String(row?.status ?? "").trim() || "live"} and sits in class `
236
+ + `${shared.join(", ")} — one of the matter's own instructed classes. "not worth the line" is a `
237
+ + `statement about the report's budget, not about this mark, and a live registration in an `
238
+ + `instructed class is never omitted for want of room. Either carry it, or decline it on something `
239
+ + `true of THIS MARK: unrelated-goods if its goods do not meet the matter's, off-field-not-major `
240
+ + `if it is off-field and not a major brand, an active dispute or a well-known enforcer, `
241
+ + `own-right if it belongs to the applicant, duplicate-of-delivered if the same right is already `
242
+ + `reported. Say what makes this mark distant, or give it its line.`;
243
+ }
244
+
213
245
  // THERE IS NO CLASS-OVERLAP REFUSAL, AND THE FIRST DRAFT HAD ONE. It refused `unrelated-goods` and
214
246
  // `off-field-not-major` over any record sharing one of the matter's filed classes, on the reasoning
215
247
  // that the driver holds both class lists and they disagree with the stated reason.
@@ -612,6 +612,26 @@ const MODEL_FAMILY_RE = /(?:^|\/)(?:claude-)?(opus|sonnet|haiku)(?:[-.]|$)/i;
612
612
  // mini one would then read as agreement, which is exactly the corruption this gauge exists to catch.
613
613
  const OPENAI_ID_RE = /^(?:openai\/)?(gpt-[\w.-]+|o\d[\w.-]*)$/i;
614
614
  const DATE_SUFFIX_RE = /-(?:\d{8}|\d{4}-\d{2}-\d{2})$/;
615
+
616
+ /**
617
+ * Is this model id a DATED SNAPSHOT, or an undated alias that a provider may repoint?
618
+ *
619
+ * `modelBasis: "actual"` says the provider answered rather than that we guessed — it does not say the
620
+ * answer names a fixed build. Measured across three archived runs: haiku came back
621
+ * `claude-haiku-4-5-20251001`, opus and sonnet came back `claude-opus-5` and `claude-sonnet-5`. All
622
+ * three were recorded identically as observed, and two of them name something the provider can move
623
+ * underneath us. A snapshot rotation behind either alias between two runs leaves EXACTLY that record and
624
+ * is invisible, which is how an A/B across time ends up eliminating the seat on evidence that could not
625
+ * have shown it either way.
626
+ *
627
+ * Returns "snapshot", "alias", or null when there is no id to judge — null is a could-not-look and is
628
+ * never collapsed into "alias". PURE.
629
+ */
630
+ export function modelSnapshotKind(model) {
631
+ const id = String(model ?? "").trim();
632
+ if (!id) return null;
633
+ return DATE_SUFFIX_RE.test(id) ? "snapshot" : "alias";
634
+ }
615
635
  export function modelFamily(model) {
616
636
  if (!model) return null;
617
637
  const resolved = resolveModel(String(model));
@@ -1120,6 +1120,10 @@ serve({
1120
1120
  properties: {
1121
1121
  recordId: { type: "string", description: "Copied VERBATIM from the filings you were given." },
1122
1122
  read: { type: "string", description: "What you concluded about THIS filing." },
1123
+ // Declared here for the reason the comment above gives: `additionalProperties: false`
1124
+ // means an undeclared key is a contradiction between the prose and the schema, and the
1125
+ // seat resolves it whichever way it happens to trust.
1126
+ band: { type: "string", description: "OPTIONAL — how you rate THIS filing, in the framework's own band words. Send it when you formed a view on the filing itself; the card prints it as that filing's rating. Omit it and your read still prints, with no rating claimed." },
1123
1127
  },
1124
1128
  },
1125
1129
  },
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
13
13
  import { join, dirname, basename } from "node:path";
14
14
  import { driverDir } from "../shared/driver-dir.mjs"; //
15
15
  import { tmpdir } from "node:os";
16
- import { config, resolveModel, modelFamily, envOn, envGateOn } from "./driver.config.mjs";
16
+ import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn } from "./driver.config.mjs";
17
17
  import { stageLog, runLog, note, outputMeta } from "./log.mjs";
18
18
  // — the closed disposition set has ONE author; this file dictates it and must not retype it.
19
19
  import { DISPOSITIONS, POSITION_REQUIRED_DISPOSITIONS } from "./findings-model.mjs";
@@ -1142,6 +1142,11 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1142
1142
  const modelRequested = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model);
1143
1143
  const modelActual = (typeof turn.modelWire === "string" && turn.modelWire) ? turn.modelWire : null;
1144
1144
  const modelBasis = modelActual ? "actual" : "unknown";
1145
+ // WHETHER THE OBSERVED ID NAMES A FIXED BUILD. `modelBasis: "actual"` says the provider answered,
1146
+ // not that the answer is pinned: two of the three tiers come back as undated aliases the provider
1147
+ // may repoint, and recorded beside a dated one they read identically. null when there is nothing to
1148
+ // judge, never collapsed into "alias".
1149
+ const modelSnapshot = modelSnapshotKind(modelActual);
1145
1150
  if (modelActual) lastModelWire = modelActual; // — never overwritten with null
1146
1151
  // The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
1147
1152
  // back as `claude-haiku-4-5-20251001`. THREE-VALUED: null when either side names no family this
@@ -1570,7 +1575,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1570
1575
  // modelMismatch — true/false when both sides name a family, null when either does not.
1571
1576
  // Written even on the rows where they are null, so "this engine cannot report" stays visibly
1572
1577
  // different from "this record predates the gauge".
1573
- modelActual, modelBasis, modelMismatch,
1578
+ modelActual, modelBasis, modelSnapshot, modelMismatch,
1574
1579
  // W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
1575
1580
  // records INTENT (the mode the engine was configured to bill under), not independent billing evidence
1576
1581
  // — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
@@ -1705,7 +1710,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1705
1710
  event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: !fail, fail: fail ?? null,
1706
1711
  //: the spine carries the same pair as the per-stage log, or the two disagree about what
1707
1712
  // ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
1708
- model: modelRequested, modelActual, modelBasis, modelMismatch,
1713
+ model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
1709
1714
  wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
1710
1715
  rescued: rescued ?? undefined, killed: killed || undefined,
1711
1716
  quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
@@ -154,7 +154,11 @@ const DECLARED = Object.freeze({
154
154
  // "the lane that produced the record is a driver fact". The chip is derived from this joined list and
155
155
  // from the finding's own receipted evidence — never from a word the seat typed about itself.
156
156
  "marks.findings": ["ordinal", "name", "owner", "band", "net", "type", "evidence", "basis", "weighedFilings"],
157
- "marks.registerReads": ["recordId", "read"],
157
+ // `band` is the rater's rating OF THAT FILING, optional, in the framework's own ladder words. It is
158
+ // declared here as well as in the tool schema because this allowlist — not the schema — is what the
159
+ // driver validates against: a key the seat sends and this list omits is refused, so the read would
160
+ // arrive banded and land unbanded with nothing saying why.
161
+ "marks.registerReads": ["recordId", "read", "band"],
158
162
  "marks.negatives": ["term", "source", "note"],
159
163
  });
160
164
 
@@ -2,7 +2,7 @@
2
2
  "name": "prelim-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.1",
5
+ "version": "0.3.0-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": {
@@ -107,7 +107,8 @@ import { mintCrossCheckDoubts, mintContradictionDoubts, stitchDoubts, applyClosu
107
107
  import { readAcceptedClosures } from "./doubt-closure-tool.mjs";
108
108
  import { CLOSURE_EVIDENCE_FILES } from "./doubt-closure-call.mjs";
109
109
  import { doubtsForClosure, doubtSelectionNote } from "./doubt-selection.mjs"; // doubt-closure selection
110
- import { deriveAsks, applyAskClosure, finalizeOpenHandoffs, summarizeAsks } from "./ask-ledger.mjs"; // PR-6 — every ask ends (2026-07-29)
110
+ import { deriveAsks, applyAskClosure, finalizeOpenHandoffs, summarizeAsks } from "./ask-ledger.mjs";
111
+ import { pendingWhatIf, claimWhatIf, finishWhatIf, whatIfRefusal } from "./whatif-queue.mjs"; // tracker issue 240 — a queued job is settled before its run archives // PR-6 — every ask ends (2026-07-29)
111
112
  import { mintPresenceDoubts } from "./presence-reconciliation.mjs"; // presence-or-reason (2026-07-22 — the unjoined-Sheet-2 leak)
112
113
  import { escalatedAxes } from "./skeptic-record.mjs"; // THE escalation parse — shared with the record_skeptic transport so the rendered shape and this read cannot drift
113
114
  // — every placed candidate ends somewhere a reader can see; the ones that do not are counted by name
@@ -1532,6 +1533,13 @@ function attachProfile(ctx, job, { write = true } = {}) {
1532
1533
  const current = resolveProfile(job);
1533
1534
  if (current && current.key !== sidecar.profileKey)
1534
1535
  runLog(ctx.paths.runDir, { event: "profile-mismatch", sidecar: sidecar.profileKey, resolved: current.key });
1536
+ // THE MATCH IS RECORDED TOO, and that is the point rather than symmetry for its own sake.
1537
+ // `profile-mismatch` existed and its counterpart did not, so "the profile was right" was carried
1538
+ // by the ABSENCE of a row — and an absence cannot tell "it matched" from "the probe never ran"
1539
+ // (it throws below on a broken profiles/ state) or from "this run predates the probe". Three
1540
+ // different facts, one empty grep. The positive row says which.
1541
+ else if (current)
1542
+ runLog(ctx.paths.runDir, { event: "profile-resolved", sidecar: sidecar.profileKey, resolved: current.key });
1535
1543
  } catch (e) {
1536
1544
  runLog(ctx.paths.runDir, { event: "profile-mismatch-probe-failed", error: String(e.message).slice(0, 120) });
1537
1545
  }
@@ -6135,8 +6143,70 @@ function sentinel(runDir, name, obj) {
6135
6143
  atomicWrite(join(runDir, name), JSON.stringify({ ts: new Date().toISOString(), ...obj }, null, 2) + "\n");
6136
6144
  }
6137
6145
 
6146
+ /**
6147
+ * Settle every queued what-if BEFORE the run dir moves into the archive — tracker issue 240.
6148
+ *
6149
+ * A queued job lives under the run directory, and the worker enumerates candidates with `liveRunDirs`,
6150
+ * which skips `archive` outright. So archiving carried a pending job out of the worker's reach: not
6151
+ * claimed, not settled, not refused. The client who asked for it got no answer and no explanation, and
6152
+ * no row anywhere said why. That is an absence reported as nothing at all — the one shape
6153
+ * `whatIfRefusal` exists to prevent.
6154
+ *
6155
+ * BEFORE the rename, deliberately: the settlement is written into the run dir and travels with it, so
6156
+ * the answer is on disk in the archive where the read tool will look for it.
6157
+ *
6158
+ * ✕ THE SENTENCE IS PER KIND, and a memo does not get the stage one. `whatIfRefusal`'s standing
6159
+ * refusal — "this run is delivered or archived, what-if runs on live runs only" — is TRUE of a stage
6160
+ * and FALSE of a memo, which runs on archived evidence by design. Telling a client their memo was
6161
+ * refused because the run is archived would be a false sentence in the one place they go to find out
6162
+ * what happened. A memo is closed on the true reason instead: it was queued before the run archived,
6163
+ * and the drain does not reach an archived run.
6164
+ *
6165
+ * ✕ A JOB ANOTHER WORKER IS ACTIVELY RUNNING IS LEFT ALONE. `pendingWhatIf` returns queued jobs and
6166
+ * claims older than an hour, never a fresh claim, so a live worker's job is not settled underneath it.
6167
+ * A fresh claim at the moment of archiving is a race this does not close, and it is written down
6168
+ * rather than papered over.
6169
+ *
6170
+ * Best-effort throughout: a failure to settle must never stop the archive, because a run that cannot
6171
+ * archive is a much larger problem than a job with no row.
6172
+ */
6173
+ export function settlePendingWhatIfsBeforeArchive(run) {
6174
+ let pending = [];
6175
+ // NOT a silent catch. This function exists because a job went unanswered with no row anywhere; a
6176
+ // failure to enumerate that returned quietly would reproduce exactly that, one level up, and the
6177
+ // archive would still succeed so nothing downstream would look wrong.
6178
+ try { pending = pendingWhatIf(run.runDir); }
6179
+ catch (e) { note(`what-if settle before archive could not enumerate the queue (${String(e?.message ?? e).slice(0, 120)}) — pending jobs may be unanswered`); return; }
6180
+ for (const entry of pending) {
6181
+ try {
6182
+ // CLAIM FIRST, exactly as the worker does. `finishWhatIf` writes the terminal file and does not
6183
+ // remove the `.json`; the claim is what renames it out of the queue. Settling without claiming
6184
+ // writes a `.failed` beside a `.json` that still reads as QUEUED to every reader — the job would
6185
+ // look pending and answered at the same time, which is worse than the silence being fixed here.
6186
+ const job = claimWhatIf(entry);
6187
+ if (!job) continue; // another worker won it, or it vanished — not ours to settle
6188
+ const kind = job?.op?.kind === "memo" ? "memo" : "stage";
6189
+ // ✕ THE SENTENCE IS PER KIND. The standing refusal — "what-if runs on live runs only" — is TRUE
6190
+ // of a stage and FALSE of a memo, which reasons over archived evidence by design. A memo is
6191
+ // closed on the true reason instead.
6192
+ const error = kind === "memo"
6193
+ ? "this what-if was queued before its run was archived, and the drain does not reach an archived run — ask it again against the archived run, which a memo may read"
6194
+ : (whatIfRefusal({ location: "archive", state: run.state ?? null, kind }) ?? "this run is delivered or archived — what-if runs on live runs only");
6195
+ finishWhatIf(run.runDir, entry.id, { ok: false, op: job.op ?? null, error });
6196
+ try { runLog(run.runDir, { event: "whatif-settled-on-archive", id: entry.id, kind, stale: entry.stale }); }
6197
+ catch { /* the row is written; the log line is a convenience and must not undo it */ }
6198
+ } catch (e) {
6199
+ // One job's settlement must not cost the others, or the archive — but it must not be SILENT
6200
+ // either. A swallowed failure here leaves exactly the unanswered job this function exists to
6201
+ // prevent, and the archive still succeeds, so nothing downstream looks wrong.
6202
+ note(`what-if ${entry.id} could not be settled before archive (${String(e?.message ?? e).slice(0, 120)}) — it may be unanswered`);
6203
+ }
6204
+ }
6205
+ }
6206
+
6138
6207
  function archive(run) {
6139
6208
  try {
6209
+ settlePendingWhatIfsBeforeArchive(run);
6140
6210
  mkdirSync(dirname(run.archiveDir), { recursive: true });
6141
6211
  renameSync(run.runDir, run.archiveDir);
6142
6212
  return run.archiveDir;
@@ -13221,7 +13291,7 @@ async function pipelineInner(job, opts = {}) {
13221
13291
  // one, because the sibling's population boundary is deliberate and correct. It covers a
13222
13292
  // finding-ending followed by SILENCE; this covers one followed by a SENTENCE.
13223
13293
  //
13224
- // On R2 `russet-kestrel` the sibling logged `checked:5 matched:5 lost:0` — correctly — on a
13294
+ // On a delivered R2 run the sibling logged `checked:5 matched:5 lost:0` — correctly — on a
13225
13295
  // delivery missing two marks from the lawyer's final list, one rated HIGH. They were dropped with
13226
13296
  // a reason, so they were outside its population by design. A drop with a reason nobody verifies
13227
13297
  // is not accounted for; it is unexamined with a paper trail, and that delivery carried 66 of them.
@@ -15520,6 +15590,17 @@ export async function runExperiment(job, opts) {
15520
15590
  // what RAN.
15521
15591
  engine: experimentEngineName(),
15522
15592
  modelTier: model ?? null,
15593
+ // — THE RATING AUTHORITY THIS ARM RAN UNDER, on the arm's own record.
15594
+ //
15595
+ // `whatIfRun` resolves this correctly and returned it in memory only: nothing in the experiment
15596
+ // directory named the profile it rated under, so a reader coming to the arm tomorrow could not
15597
+ // confirm which framework produced it, and a future regression would be as silent as the one this
15598
+ // was opened on. The frozen sidecar is the authority — never a fresh resolve, which is the mid-run
15599
+ // drift the freeze exists to forbid.
15600
+ //
15601
+ // `null` means the run carries no frozen profile (a legacy run), and it is written rather than
15602
+ // omitted: absent and "there was none" are different facts and only one of them is a defect.
15603
+ ratedUnder: ctx.profile?.profileKey ?? null,
15523
15604
  dispatchTrigger: opts.dispatchTrigger ?? "fresh",
15524
15605
  // — WHICH PASS THIS ARM REPRODUCED, in a word. null on a non-corrective arm;
15525
15606
  // never "dispatched-warm", which is production's alone.