clearotron 0.2.2 → 0.2.4

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 (161) hide show
  1. package/.env.example +64 -37
  2. package/.nvmrc +1 -1
  3. package/CONTRIBUTING.md +8 -4
  4. package/INSTALL.md +36 -14
  5. package/README.md +18 -22
  6. package/THIRD-PARTY-NOTICES.md +1 -1
  7. package/bin/clearotron.mjs +22 -0
  8. package/bin/example.mjs +10 -5
  9. package/bin/onboard.mjs +370 -28
  10. package/bin/start.mjs +220 -12
  11. package/build-info.json +2 -2
  12. package/demo/README.md +1 -1
  13. package/demo/full-country-search/run/_driver/framework.json +1 -1
  14. package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
  15. package/demo/knockout-search/run/_driver/framework.json +1 -1
  16. package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
  17. package/docs/E2E.md +5 -5
  18. package/docs/GLOSSARY.md +85 -0
  19. package/docs/PORTAL.md +2 -2
  20. package/docs/README.md +6 -4
  21. package/docs/SECURITY.md +17 -0
  22. package/docs/architecture/01-product-overview.md +21 -9
  23. package/docs/architecture/04-configuration-reference.md +2 -0
  24. package/docs/architecture/05-config-governance.md +19 -0
  25. package/docs/architecture/05-customer-profiles.md +2 -2
  26. package/docs/configuration.md +4 -3
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
  28. package/driver/CHANGELOG.md +90 -0
  29. package/driver/README.md +25 -6
  30. package/driver/common-law-receipts.mjs +1 -1
  31. package/driver/config-staleness.mjs +1 -1
  32. package/driver/connotation-search.mjs +1 -1
  33. package/driver/contract-audit.mjs +5 -1
  34. package/driver/contract-e3-baseline.json +11 -11
  35. package/driver/dev-portal.mjs +2 -2
  36. package/driver/doubt-selection.mjs +1 -1
  37. package/driver/drainer-identity.mjs +1 -1
  38. package/driver/driver.config.mjs +1 -1
  39. package/driver/effort-model.mjs +2 -2
  40. package/driver/engine/cli-version.mjs +103 -0
  41. package/driver/engine/probe.mjs +45 -5
  42. package/driver/enqueue-schema.mjs +8 -8
  43. package/driver/feedback-store.mjs +1 -1
  44. package/driver/findings-model.mjs +52 -2
  45. package/driver/flag-snapshot.mjs +20 -0
  46. package/driver/framework.mjs +1 -1
  47. package/driver/gateway.mjs +20 -3
  48. package/driver/outbox-backoff.mjs +1 -1
  49. package/driver/package.json +3 -3
  50. package/driver/party-facts.mjs +2 -2
  51. package/driver/phase0.mjs +1 -1
  52. package/driver/pipeline.mjs +211 -106
  53. package/driver/plain-register.mjs +134 -0
  54. package/driver/portal-access.mjs +1 -1
  55. package/driver/portal-config-view.mjs +70 -2
  56. package/driver/portal-families.mjs +1 -1
  57. package/driver/portal-local-auth.mjs +5 -1
  58. package/driver/portal-report.mjs +1 -1
  59. package/driver/portal-service.mjs +79 -9
  60. package/driver/portal-upstream.mjs +2 -2
  61. package/driver/predelivery-lint.mjs +151 -2
  62. package/driver/profile-page.html +10 -2
  63. package/driver/profile-service.mjs +4 -4
  64. package/driver/profiles/README.md +3 -3
  65. package/driver/profiles.mjs +144 -12
  66. package/driver/publish/index.mjs +2 -2
  67. package/driver/publish/knockout.mjs +7 -1
  68. package/driver/publish/render-knockout.mjs +453 -118
  69. package/driver/publish/render.mjs +111 -16
  70. package/driver/reference-strip-signatures.mjs +20 -0
  71. package/driver/search-policy.mjs +3 -3
  72. package/driver/skills/blind-frame/SKILL.md +1 -1
  73. package/driver/skills/knockout-assess/SKILL.md +84 -0
  74. package/driver/skills/matter-frame/SKILL.md +6 -6
  75. package/driver/skills/matter-frame/watchlist-reference.md +2 -2
  76. package/driver/skills/prelim-common-law/SKILL.md +1 -1
  77. package/driver/skills/prelim-search/SKILL.md +1 -1
  78. package/driver/skills/prelim-search/report-prose.md +82 -0
  79. package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
  80. package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
  81. package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
  82. package/driver/skills/prelim-search/synthesis-rules.md +1 -1
  83. package/driver/skills/prelim-variants/SKILL.md +3 -3
  84. package/driver/stage-context.mjs +13 -0
  85. package/driver/stages.mjs +54 -7
  86. package/driver/suite-census.json +220 -58
  87. package/driver/systemd/clearotron-worker.service +3 -3
  88. package/driver/tokens.mjs +1 -1
  89. package/driver/unit-inventory.mjs +214 -27
  90. package/driver/verify.mjs +1 -1
  91. package/driver/whatif-memo-run.mjs +26 -4
  92. package/driver/whatif-memo.mjs +19 -1
  93. package/examples/README.md +2 -2
  94. package/examples/grants.example.json +6 -7
  95. package/mcp-server/CHANGELOG.md +4 -0
  96. package/mcp-server/lib/scrub.mjs +1 -1
  97. package/mcp-server/lib/whatif.mjs +2 -2
  98. package/mcp-server/package.json +1 -1
  99. package/mcp-server/packs/ops/EXAMPLES.md +2 -2
  100. package/mcp-server/server.mjs +1 -1
  101. package/package.json +31 -19
  102. package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
  103. package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
  104. package/portal-ui/dist/index.html +2 -2
  105. package/portal-ui/package.json +1 -1
  106. package/providers/_shared/term-shape.mjs +1 -1
  107. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  108. package/providers/oauth-mcp-bridge/package.json +1 -1
  109. package/providers/uspto-local/src/index-store.js +2 -2
  110. package/scripts/added-reference-check.mjs +45 -89
  111. package/scripts/citation-line-check.mjs +121 -2
  112. package/scripts/e2e.mjs +2 -2
  113. package/scripts/env-classify.mjs +67 -3
  114. package/scripts/generated-files-are-current.mjs +125 -0
  115. package/scripts/live-surface-check.mjs +38 -1
  116. package/scripts/mint-names-in-force.mjs +4 -2
  117. package/scripts/mint-public-residue.mjs +74 -0
  118. package/scripts/mint-reference-strip-backlog.mjs +12 -1
  119. package/scripts/mint-suite-census.mjs +5 -2
  120. package/scripts/nightly-notice.mjs +0 -2
  121. package/scripts/preinstall-node-check.mjs +24 -0
  122. package/scripts/render-check.mjs +13 -2
  123. package/scripts/report-frame-check.mjs +8 -1
  124. package/scripts/report-screenshot.mjs +6 -3
  125. package/scripts/revisit-render-check.mjs +6 -3
  126. package/scripts/score.mjs +1 -1
  127. package/scripts/strip-tracker-citations.mjs +38 -7
  128. package/scripts/test-full.mjs +226 -0
  129. package/scripts/test-run.mjs +16 -0
  130. package/shared/README.md +2 -1
  131. package/shared/anon-overlay.mjs +3 -3
  132. package/shared/browser-temp-root.mjs +142 -0
  133. package/shared/checkout-move.mjs +1 -1
  134. package/shared/client-door.mjs +4 -3
  135. package/shared/customer-store.mjs +1 -1
  136. package/shared/identifier-scan.mjs +1 -1
  137. package/shared/install-auth.mjs +35 -0
  138. package/shared/invocation.mjs +33 -1
  139. package/shared/listen.mjs +55 -3
  140. package/shared/names-in-force.mjs +6 -1
  141. package/shared/node-floor.mjs +97 -0
  142. package/shared/os-advice.mjs +91 -0
  143. package/shared/reap-on-exit.mjs +42 -0
  144. package/shared/reference-guard-classes.mjs +351 -0
  145. package/shared/staff-domain.mjs +154 -0
  146. package/shared/store-in-repo.mjs +23 -2
  147. package/shared/suite-census.mjs +31 -3
  148. package/shared/withheld-paths-access.mjs +36 -18
  149. package/skills/clearotron-ops/SKILL.md +1 -1
  150. package/driver/recipes/README.md +0 -49
  151. package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
  152. package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
  153. package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
  154. package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
  155. package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
  156. package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
  157. package/scripts/ai-page-render-check.mjs +0 -532
  158. package/scripts/clearances-render-check.mjs +0 -480
  159. package/scripts/composer-render-check.mjs +0 -1027
  160. package/scripts/home-render-check.mjs +0 -526
  161. package/scripts/portal-lifecycle-check.mjs +0 -639
@@ -123,7 +123,13 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
123
123
  if (!tuple) return v("failed", "none", `${id} returned nothing`, "The adapter settled no tuple — this is a driver bug, not a configuration one.");
124
124
  const s = tuple.signals ?? {};
125
125
  const text = `${tuple.stderr ?? ""}\n${tuple.stdout ?? ""}`;
126
- const detail = tail(tuple.stderr);
126
+ // READ FROM THE SAME STREAMS THE CLASSIFICATION IS. Every branch below matches over `text`, which is
127
+ // stderr AND stdout, while this carried stderr alone — so a CLI that reports its failure on stdout
128
+ // produced a verdict with its evidence dropped, and the reader got a headline with nothing under it.
129
+ // stderr stays FIRST because that is where a diagnostic belongs and where these CLIs put theirs; the
130
+ // fallback exists so that "the engine said nothing" is a claim about the engine rather than about
131
+ // which pipe this happened to look at.
132
+ const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
127
133
 
128
134
  if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null };
129
135
 
@@ -249,13 +255,47 @@ export function probeWeatherWarning(verdict) {
249
255
  * whatever the operator's shell happens to have set and report a pass for the wrong engine — the same
250
256
  * frozen-provider trap `preflightCandidate` works around one file over.
251
257
  *
252
- * ONLY the engine-selection keys are applied. Credentials and billing-mode variables are deliberately
253
- * NOT copied: the probe must bill exactly the way a run on this box would, and moving a spend variable
254
- * to make a probe pass is the one thing this must never do.
258
+ * THE SPEND VARIABLES ARE APPLIED TOO, and the rule this replaces had it backwards. It said they were
259
+ * deliberately not copied, so that "the probe must bill exactly the way a run on this machine would"
260
+ * the aim is right and withholding them defeated it. The caller supplies the lane and the credential
261
+ * precisely because they are not in the environment yet; not copying them left the probe billing the
262
+ * way the operator's shell happened to be set, while the run that followed billed the way the reader
263
+ * had just chosen. The probe proved a lane nobody was going to use. Measured: an API-key install failed
264
+ * as "not signed in" because the adapter removes the key under any mode that is not the API-key one,
265
+ * and the mode it read was the unset one.
266
+ *
267
+ * WHAT IS STILL NEVER DONE, which is the sentence that was worth keeping: nothing here defaults,
268
+ * repairs, invents or falls back. It copies what the caller passed and nothing else, so a probe cannot
269
+ * pass on a credential a run would not have — the caller is the wizard about to write these very values
270
+ * to disk, or a command reading the values already there.
271
+ *
272
+ * AND IT PUTS EVERYTHING BACK. Widening what this writes onto the process widens what it must remove
273
+ * again; a credential outliving the check that borrowed it would be a worse defect than the one this
274
+ * fixed, and a check drives the restore rather than trusting it.
255
275
  */
256
276
  function applyEngineEnv(env) {
257
277
  if (!env || env === process.env) return () => {};
258
- const keys = ["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES).map((s) => s.env)];
278
+ // EVERY VARIABLE THAT DECIDES WHAT THE SPAWN IS, not only the ones that decide WHICH BINARY.
279
+ //
280
+ // This list used to carry CLEAROTRON_AI and the binary paths. It did not carry the billing mode or
281
+ // the API key — so a caller could hand this function a correct api-key environment, watch
282
+ // `resolveAuthMode` accept it, and then have `runTurn` read `process.env`, where neither had arrived.
283
+ // `anthropic-agent.spawnEnv` deletes ANTHROPIC_API_KEY under any mode that is not api-key, and the
284
+ // mode it read was unset, so it deleted the key the reader had just typed. The turn then failed as
285
+ // "not signed in" — correctly, about an environment nobody had asked for.
286
+ //
287
+ // Reported from a real WSL2 install, 2026-09-09: the setup wizard's api-key lane, a valid key, and a
288
+ // sign-in failure the reader could only get past by exporting the key into their own shell.
289
+ //
290
+ // A SET, AND THE DUPLICATE IS NOT HYPOTHETICAL. Both engines name the same billing variable, so this
291
+ // list contains CLEAROTRON_AI_BILLING twice — and the save below is `saved.set(k, process.env[k])`
292
+ // inside the same loop that writes. On the second visit it saved the value the FIRST visit had just
293
+ // written, so the restore put that back instead of deleting it, and the mode leaked into the rest of
294
+ // the process. Caught by the arm that exists to prove the restore, which is the only reason widening
295
+ // this list was safe to do at all.
296
+ const keys = [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
297
+ .flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
298
+ .filter(Boolean)])];
259
299
  const saved = new Map();
260
300
  for (const k of keys) {
261
301
  saved.set(k, process.env[k]);
@@ -237,7 +237,7 @@ function niceClassErrors(list, label) {
237
237
  // and reaches the report body, where it REVERSES THE DISPLAY of everything after it. The report is a
238
238
  // legal deliverable that goes to a client.
239
239
  //
240
- // A zero-width joiner makes "AUR\u200DORA" render as AURORA and compare unequal to it, so
240
+ // A zero-width joiner makes "IRON\u200DWHISK" render as IRONWHISK and compare unequal to it, so
241
241
  // `selfExclusionOwners` and dedup both miss a mark a human reads as the account's own.
242
242
  //
243
243
  // NFD "SIRÈNE" !== NFC "SIRÈNE", so one mark submitted two ways is two matters, two searches, two bills.
@@ -285,7 +285,7 @@ const markNameSites = (job) => {
285
285
  for (const f of ["markName", "name"]) if (job?.[f] != null) sites.push([f, () => job[f], (v) => { job[f] = v; }]);
286
286
  if (Array.isArray(job?.marks)) job.marks.forEach((m, i) => {
287
287
  // A BARE STRING IS A MARK NAME, and this shape reaches validateJob unconverted. `assembleFromFlags`
288
- // turns `marks: ["AURORA"]` into `[{ name: "AURORA" }]`, so every door that assembles is already
288
+ // turns `marks: ["IRONWHISK"]` into `[{ name: "IRONWHISK" }]`, so every door that assembles is already
289
289
  // covered — but the runner's wall calls validateJob({ atClaim: true }) on the MANIFEST AS IT SITS ON
290
290
  // DISK, which nothing re-assembles. The kebab-collision check below reads the same shape for exactly
291
291
  // that reason. Walking only `m.name` would leave this guard covering every door except the one it
@@ -778,16 +778,16 @@ export function validateJob(job, { atClaim = false } = {}) {
778
778
  // that would drop the customer's platforms, self-exclusion seed and the framework that RATES the matter".
779
779
  // A MISSING key returns generic in silence. Both produce the same wrong deliverable, and only one says so.
780
780
  //
781
- // It has already happened: a paid Zephyr Beverages clearance was rated on the Generic default scale because the
782
- // request named the customer in prose and left the account field empty. Nothing objected. A person
783
- // noticed hours later and re-ran it.
781
+ // It has already happened: a paid clearance for a test account was rated on the Generic default scale
782
+ // because the request named the customer in prose and left the account field empty. Nothing objected.
783
+ // A person noticed hours later and re-ran it.
784
784
  //
785
785
  // WHY THIS IS NOT SIMPLY "customer present, key absent". That shape is byte-identical to a LEGITIMATE
786
786
  // third-party search — a firm asking us to clear a mark for their own client, who is not our account —
787
787
  // and the applicant deliberately never selects a profile, because a third-party search must never inherit
788
788
  // a customer's exclusions. Clarifying on that shape alone would bounce real work every day.
789
789
  //
790
- // The discriminator is the ROSTER. The Zephyr Beverages request differed in one way that matters: we hold an
790
+ // The discriminator is the ROSTER. That request differed in one way that matters: we hold an
791
791
  // account by that name. A named applicant we have no account for is a third-party search and runs
792
792
  // untouched; a named applicant who IS one of our customers, with no key, is ambiguous in a way only a
793
793
  // person can settle — did intake forget to tag it, or is this genuinely a search against a name that
@@ -965,7 +965,7 @@ export const EXAMPLE_JOB = {
965
965
  rawRequest: "<the verbatim forwarded email text, untouched — archived as inbound-request.txt (§A5)>",
966
966
  brief: "<the confirmation brief exactly as sent to the requester — archived as confirmation-brief.md>",
967
967
  deadline: "2026-06-20T17:00:00Z", // optional; drives the §A3 deadline-envelope arithmetic
968
- profileKey: "aurora", // D4.1: the customer ACCOUNT the intake AI resolved → selects the
968
+ profileKey: "demo-brand-owner", // D4.1: the customer ACCOUNT the intake AI resolved → selects the
969
969
  // profile (marketplaces/classes/delivery/appetite); omit ⇒ generic
970
970
  product: "multi-country-focus-search",// WHICH OF THE FOUR (OPTIONAL): one of products.mjs PRODUCT_IDS.
971
971
  // Omit ⇒ the project/customer defaultProduct, else the product
@@ -987,7 +987,7 @@ export const EXAMPLE_JOB = {
987
987
  projectKey: "console-ecosystem", // spec 62 (OPTIONAL): the PROJECT/engagement under the customer whose
988
988
  // overlay (its own marketplaces/classes/sector/posture) rates this
989
989
  // matter; omit ⇒ runs on the customer profile. Unknown key ⇒ clarify.
990
- customer: "Aurora Interactive", // applicant/owner → affiliate self-exclusion set (§B3.2)
990
+ customer: "Demo Brand Owner", // applicant/owner → affiliate self-exclusion set (§B3.2)
991
991
  customerUnknown: false, // B5: true when the applicant is neither stated nor forwarder-implied —
992
992
  // arms candidate-self classification + the late-bind watch (NEVER inferred from the mark)
993
993
  // caseLaw — NOT A FIELD. The case-law and opposition reading is what a Full country search IS
@@ -19,7 +19,7 @@
19
19
  // come from report-data.json; the disposition comes from findings.json beside it in the same run dir,
20
20
  // because report-data.json is the CLIENT cut and stopped serving the engine's placement key. A
21
21
  // reader who finds the ordinal now points at `KURENA / Manageable / rebuttable` when the flag says
22
- // `PETCARY / Medium / conceded` knows the run was republished, which is exactly the thing a bare id
22
+ // `BURROWELL / Medium / conceded` knows the run was republished, which is exactly the thing a bare id
23
23
  // would have hidden.
24
24
 
25
25
  import { randomUUID } from "node:crypto";
@@ -168,8 +168,8 @@ export const ACTION_KINDS = [...CONDITION_KINDS, ...ADVISORY_KINDS];
168
168
  export const CLIENT_TIER_BY_COMPOSITE = { 1: "LOW", 2: "MANAGEABLE", 3: "MEDIUM", 4: "HIGH", 5: "VERY HIGH" };
169
169
 
170
170
  // ── doc 50 — band mode (schema_version 4): the framework in force rates the matter ────────────────────
171
- // A v4 finding carries `band` — one of the frozen framework manifest's ordered band words (zephyr says
172
- // "Medium", the Generic default says "Moderate", aurora has a "Low") — and NO composite/level/
171
+ // A v4 finding carries `band` — one of the frozen framework manifest's ordered band words (one ladder
172
+ // says "Medium", the Generic default says "Moderate", another has a "Low") — and NO composite/level/
173
173
  // dispute_type (one rating authority; the retired scale is FORBIDDEN, not just optional). Presentation
174
174
  // joins on the band's TONE (a closed enum in the manifest) so 4-band and 5-band ladders both land on the
175
175
  // existing badge/gauge ramps without any per-framework code.
@@ -2441,6 +2441,56 @@ export function knockoutFindingViews(mark, { manifest = null } = {}) {
2441
2441
  * Derived through knockoutFindingViews rather than re-deriving that fallback here — one projection
2442
2442
  * decides what a finding's number is, and this cell cannot disagree with the sheet it points into.
2443
2443
  */
2444
+ // ── THE REVIEWER'S NOTES: WHAT EACH ONE IS ABOUT (tracker issues 331 A.4, 333 rule 5) ───────────────
2445
+ //
2446
+ // The knockout page prints a note about the REQUEST at the top, above the conflicts, and a note about
2447
+ // the NAME under that name's cards. A note saying the screen may have been scoped to the wrong market
2448
+ // is the most consequential line on the page and it used to sit under roughly 1,900 words.
2449
+ //
2450
+ // THE READER LIVES HERE BECAUSE TWO MODULES NEED THE SAME ANSWER. The renderer sorts the notes; the
2451
+ // predelivery lint warns a writer whose note will sort the way they did not intend. Two copies of this
2452
+ // regex would drift, and the drift would be silent — the page would file a note one way while the
2453
+ // reviewer told the writer it went the other.
2454
+ //
2455
+ // IT SORTS ON WHAT THE NOTE TALKS ABOUT, and the doctrine is written to match: a note about the request
2456
+ // NAMES the request. The word set spans both vocabularies deliberately — "dispatch" and "instructed"
2457
+ // are what runs written before tracker issue 333 say, "the request" and "was asked" are what runs
2458
+ // written after it say — so one reader serves the archive and the new doctrine at once.
2459
+ export const REQUEST_NOTE_WORDS = /\b(?:dispatch|the request|the requester|instructed|was asked)\b/i;
2460
+
2461
+ // Subjects that belong to the ASKING rather than to the name. A note on one of these that never names
2462
+ // the request will be filed under the name, which is why the lint flags it rather than the page
2463
+ // guessing: the fix is one clause in the note, and only its writer can add it.
2464
+ export const REQUEST_SUBJECT_WORDS =
2465
+ /\b(?:customer industry|client(?:'s)? (?:own )?(?:prior|earlier) use|prior use|intended goods|the goods we|wrong market|described as an? )/i;
2466
+
2467
+ /** One note, in either shape, as { text, about }. `about` is null when nothing has classified it. */
2468
+ export function knockoutNoteView(n) {
2469
+ if (n && typeof n === "object" && !Array.isArray(n)) {
2470
+ const about = String(n.about ?? "").trim().toLowerCase();
2471
+ return { text: String(n.text ?? n.note ?? "").trim(), about: about === "request" || about === "name" ? about : null };
2472
+ }
2473
+ return { text: String(n ?? "").trim(), about: null };
2474
+ }
2475
+
2476
+ /**
2477
+ * A mark's notes, split into the two places the page prints them.
2478
+ *
2479
+ * The rater's own `about` wins outright where one is given; only an unclassified note meets the word
2480
+ * reader. That precedence is the whole design: it means a run written under a doctrine that no longer
2481
+ * uses these words is answered without them.
2482
+ */
2483
+ export function splitKnockoutNotes(mark) {
2484
+ const all = (Array.isArray(mark?.purpleNotes) ? mark.purpleNotes : []).map(knockoutNoteView).filter((v) => v.text);
2485
+ const request = [];
2486
+ const name = [];
2487
+ for (const v of all) {
2488
+ const isRequest = v.about ? v.about === "request" : REQUEST_NOTE_WORDS.test(v.text);
2489
+ (isRequest ? request : name).push(v.text);
2490
+ }
2491
+ return { request, name, all: all.map((v) => v.text) };
2492
+ }
2493
+
2444
2494
  export function knockoutFindingRange(mark) {
2445
2495
  const ords = knockoutFindingViews(mark).map((v) => v.ordinal).sort((a, b) => a - b);
2446
2496
  const markName = String(mark?.name ?? "").trim();
@@ -318,6 +318,26 @@ export function postureDisagreement(snapshot, live) {
318
318
  "silent-output-change — a different engine answers differently and nobody is told");
319
319
  differ("billing mode", snapshot.engine?.billing?.mode ?? null, live.engine?.billing?.mode ?? null,
320
320
  "what a run costs, and who it is billed to");
321
+ // THE FIELD THAT DECIDES WHETHER A SEARCH CAN START, and the one this comparison could not see.
322
+ //
323
+ // The configuration page reads the LIVE posture and the New clearance screen reads this capture, so a
324
+ // box where they differ on this one boolean draws a green Engine row on one screen while the other
325
+ // replaces its start button with "no search engine is attached". That is not a hypothetical: it is
326
+ // what an outside user photographed, and he gave up on the product because the greener screen looked
327
+ // more authoritative. Both readings were correct about their own question and nothing compared them.
328
+ //
329
+ // Worse than silence, before this line: `disagrees` came back `[]` — which this page renders as "the
330
+ // last run ran under this same configuration". The one field they actually disagreed on was not in the
331
+ // comparison, so the page positively affirmed agreement while the two surfaces contradicted each other.
332
+ // STRINGS, NOT THE BOOLEANS THEMSELVES. Every other row here compares names and modes, so the browser
333
+ // contract parses `capture` and `live` with `asString` — handed `false` it yields null, and the row
334
+ // would reach the page with its two values blank and only the effect sentence left. The words are also
335
+ // the better answer for a reader: "found" against "not found" says it without a legend.
336
+ const found = (v) => (v === true ? "found" : v === false ? "not found" : null);
337
+ differ("engine program", found(snapshot.engine?.binaryPresent), found(live.engine?.binaryPresent),
338
+ "whether a NEW search can start — the engine that last ran and this deployment do not agree that the "
339
+ + "engine program can be found, so one screen offers a search the other refuses. Restart the engine "
340
+ + "service so it re-reads its PATH, or install the CLI where the service can see it");
321
341
 
322
342
  // Flags: compare only names BOTH sides declare, for the same reason `differ` skips absent values —
323
343
  // a build that adds a flag must not read as every older capture disagreeing with it.
@@ -102,7 +102,7 @@ export function parseFrameworkManifest(raw) {
102
102
  // `rootDir` may be a string (legacy: join against it) OR a resolver function taking the manifest's
103
103
  // skills-relative path and returning an absolute one — the layered overlay-over-base lookup
104
104
  // (driver.config.resolveSkillPath). The driver MUST resolve a framework exactly as the agent does, or the
105
- // two read different files: that divergence is what killed the first Aurora Interactive run (framework_manifest_missing
105
+ // two read different files: that divergence is what killed a test account's first run (framework_manifest_missing
106
106
  // against the bundled tree while the agent was pointed at the config store).
107
107
  export function loadFrameworkManifest(rootDir, fwPath) {
108
108
  const rel = manifestPathFor(fwPath);
@@ -13,7 +13,8 @@ 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, modelSnapshotKind, envOn, envGateOn } from "./driver.config.mjs";
16
+ import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn, preflightEngineBinary } from "./driver.config.mjs";
17
+ import { probeCliVersion } from "./engine/cli-version.mjs";
17
18
  import { stageLog, runLog, note, outputMeta } from "./log.mjs";
18
19
  // — the closed disposition set has ONE author; this file dictates it and must not retype it.
19
20
  import { DISPOSITIONS, POSITION_REQUIRED_DISPOSITIONS } from "./findings-model.mjs";
@@ -908,8 +909,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
908
909
  // until the wall. ONE free rung per ladder is the concession; a second zero-usage warm turn in the
909
910
  // same ladder is charged, and the record says so rather than leaving the difference to be inferred.
910
911
  //
911
- // PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22,
912
- // overwatch), not a measured optimum. Nobody has yet counted how often a second free rung would have
912
+ // PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22),
913
+ // not a measured optimum. Nobody has yet counted how often a second free rung would have
913
914
  // converted, and until someone does, 1 is the smallest concession that closes the defect.
914
915
  //
915
916
  // THE CHARGING BRANCH CANNOT FIRE TODAY, and that is stated rather than left to be discovered. `warm`
@@ -1147,6 +1148,20 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1147
1148
  // may repoint, and recorded beside a dated one they read identically. null when there is nothing to
1148
1149
  // judge, never collapsed into "alias".
1149
1150
  const modelSnapshot = modelSnapshotKind(modelActual);
1151
+ // WHICH BUILD OF THE TOOL SERVED THIS TURN. `modelSnapshot` above answers "did the model move";
1152
+ // this answers "did the tool move", and the two have one symptom — a run whose judgment differs from
1153
+ // last week's. Captured at dispatch rather than read back afterwards, because the binary a reader
1154
+ // could probe later is not necessarily the one that ran.
1155
+ //
1156
+ // Written as an object with its own `probe` field, so an unreadable version is a RECORDED state
1157
+ // rather than a missing one. Omitting it on failure would collapse "asked and could not say" into
1158
+ // "written before anybody asked", which is the distinction the field exists for. One spawn per
1159
+ // binary per process; a probe never throws, because taking down a dispatch to record a version
1160
+ // would be a worse defect than the gap it closes.
1161
+ const cli = (() => {
1162
+ try { return probeCliVersion(preflightEngineBinary(process.env)?.resolved ?? null); }
1163
+ catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160) }; }
1164
+ })();
1150
1165
  if (modelActual) lastModelWire = modelActual; // — never overwritten with null
1151
1166
  // The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
1152
1167
  // back as `claude-haiku-4-5-20251001`. THREE-VALUED: null when either side names no family this
@@ -1576,6 +1591,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1576
1591
  // Written even on the rows where they are null, so "this engine cannot report" stays visibly
1577
1592
  // different from "this record predates the gauge".
1578
1593
  modelActual, modelBasis, modelSnapshot, modelMismatch,
1594
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1579
1595
  // W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
1580
1596
  // records INTENT (the mode the engine was configured to bill under), not independent billing evidence
1581
1597
  // — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
@@ -1711,6 +1727,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1711
1727
  //: the spine carries the same pair as the per-stage log, or the two disagree about what
1712
1728
  // ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
1713
1729
  model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
1730
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1714
1731
  wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
1715
1732
  rescued: rescued ?? undefined, killed: killed || undefined,
1716
1733
  quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
@@ -274,7 +274,7 @@ export function settleProgress(agent, now = Date.now()) {
274
274
  // disposed of on the box.
275
275
  //
276
276
  // **Owner ruling, 2026-08-24, SUPERSEDING THIS SWEEP'S HALF OF IT: failed runs' notification packets get
277
- // the same re-drop cover as delivered ones.** (Relayed by role-overwatch.)
277
+ // the same re-drop cover as delivered ones.**
278
278
  //
279
279
  // BOTH ARE KEPT BECAUSE BOTH ARE STILL TRUE, and reading them as a reversal is the mistake to avoid. A
280
280
  // failed run owes the CLIENT no report — that is 2026-08-22, and `scripts/e2e.mjs`'s delivery assertion
@@ -2,11 +2,11 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.2",
5
+ "version": "0.2.4",
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": {
9
- "node": ">=22.19.0"
9
+ "node": ">=22.13.0"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "node ../scripts/test-run.mjs node ../scripts/test-fast.mjs test/*.test.mjs",
@@ -14,6 +14,6 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "exceljs": "^4.4.0",
17
- "undici": "^8.10.1"
17
+ "undici": "^7.29.1"
18
18
  }
19
19
  }
@@ -226,8 +226,8 @@ const corpusHasKind = (set, kind) => {
226
226
  /**
227
227
  * The form of a party's name to match ENTRIES against.
228
228
  *
229
- * Register owners carry legal forms ("Aurora Therapeutics SA"); web candidate titles carry whatever the
230
- * web uses ("Aurora Therapeutics"). That asymmetry is systematic — one corpus is the register, the other
229
+ * Register owners carry legal forms ("Foxglade Therapeutics SA"); web candidate titles carry whatever the
230
+ * web uses ("Foxglade Therapeutics"). That asymmetry is systematic — one corpus is the register, the other
231
231
  * is search results — so requiring the full string admits SOME entries about a party and excludes others
232
232
  * purely by name form. A half-scoped corpus is the failure this scoping was built to avoid, inverted: it
233
233
  * still reports a contradiction, now against a source the run plainly holds.
package/driver/phase0.mjs CHANGED
@@ -87,7 +87,7 @@ export function todayISO(now = new Date()) {
87
87
  }
88
88
 
89
89
  // The resolved customer = the profile key (profiles.mjs resolveProfile, forwarder-domain only). This was
90
- // a aurora-interactive.example-vs-generic HARDCODE (D4.1); the engine is already profile-driven, so resolveProfile is
90
+ // a customer-vs-generic HARDCODE (D4.1); the engine is already profile-driven, so resolveProfile is
91
91
  // the single source of truth. ctx.customer is telemetry-only (the run-start log line); per-customer
92
92
  // DELIVERY reads the FROZEN sidecar ctx.profile.profileKey (which wins on resume) — never read
93
93
  // ctx.customer for a delivery decision, or a profiles/ edit between run and resume would diverge.