clearotron 0.3.1-beta.0 → 0.3.1-beta.2

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 (73) hide show
  1. package/.env.example +17 -0
  2. package/INSTALL.md +1 -1
  3. package/README.md +6 -1
  4. package/THIRD-PARTY-NOTICES.md +4 -4
  5. package/bin/connect.mjs +2 -2
  6. package/bin/onboard.mjs +96 -11
  7. package/bin/start.mjs +129 -23
  8. package/build-info.json +2 -2
  9. package/docs/architecture/05-config-governance.md +14 -0
  10. package/driver/CHANGELOG.md +52 -0
  11. package/driver/citation-census.json +2 -1
  12. package/driver/common-law-receipts.mjs +78 -1
  13. package/driver/contract-vocabulary.mjs +6 -6
  14. package/driver/engine/anthropic-agent.mjs +75 -0
  15. package/driver/form-neighbourhood.mjs +44 -2
  16. package/driver/matter-frame-record.mjs +68 -1
  17. package/driver/named-band.mjs +1 -1
  18. package/driver/package.json +1 -1
  19. package/driver/pipeline-knockout.mjs +13 -6
  20. package/driver/pipeline.mjs +48 -34
  21. package/driver/portal-config-view.mjs +6 -0
  22. package/driver/portal-mcp-client.mjs +54 -9
  23. package/driver/portal-service.mjs +69 -6
  24. package/driver/profile-page.html +34 -4
  25. package/driver/profiles.mjs +5 -1
  26. package/driver/register-availability.mjs +2 -2
  27. package/driver/register-plan.mjs +22 -0
  28. package/driver/scope-ledger.mjs +33 -0
  29. package/driver/skills/prelim-common-law/SKILL.md +3 -1
  30. package/driver/skills/prelim-search/synthesis-rules.md +3 -1
  31. package/driver/stages.mjs +10 -1
  32. package/driver/suite-census.json +82 -58
  33. package/mcp-server/CHANGELOG.md +16 -0
  34. package/mcp-server/http-server.mjs +27 -4
  35. package/mcp-server/lib/cf-access.mjs +16 -2
  36. package/mcp-server/lib/http-handler.mjs +37 -1
  37. package/mcp-server/lib/ops.mjs +22 -9
  38. package/mcp-server/lib/options.mjs +1 -1
  39. package/mcp-server/lib/plan.mjs +47 -2
  40. package/mcp-server/package.json +1 -1
  41. package/mcp-server/server.mjs +29 -4
  42. package/package.json +1 -1
  43. package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-D5WAoLZI.css} +20 -6
  44. package/portal-ui/dist/assets/{index-CwPAS0we.js → index-D8ITW-aD.js} +2151 -744
  45. package/portal-ui/dist/index.html +2 -2
  46. package/portal-ui/package.json +6 -6
  47. package/providers/_shared/term-shape.mjs +6 -0
  48. package/providers/_shared/territory-codes.mjs +43 -0
  49. package/providers/clarivate/src/capabilities.js +13 -1
  50. package/providers/clarivate/src/core.js +21 -1
  51. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  52. package/providers/oauth-mcp-bridge/package.json +1 -1
  53. package/scripts/citation-line-check.mjs +67 -21
  54. package/scripts/e2e.mjs +4 -2
  55. package/scripts/import-cycle-check.mjs +34 -3
  56. package/scripts/live-surface-check.mjs +2 -2
  57. package/scripts/mint-names-in-force.mjs +21 -2
  58. package/scripts/purge-runs.mjs +114 -3
  59. package/scripts/revisit-render-check.mjs +77 -44
  60. package/scripts/test-run.mjs +7 -0
  61. package/scripts/travelling-predicates.mjs +1 -1
  62. package/shared/connect-clients.mjs +95 -16
  63. package/shared/invocation.mjs +25 -0
  64. package/shared/mcp-challenge.mjs +32 -0
  65. package/shared/names-in-force.mjs +2 -0
  66. package/shared/notice-owed.mjs +62 -0
  67. package/shared/scope.mjs +66 -5
  68. package/shared/stdio-connect.mjs +92 -24
  69. package/shared/trigger-lane.mjs +19 -0
  70. package/shared/wsl.mjs +15 -0
  71. package/skills/clearotron-account/SKILL.md +27 -0
  72. package/skills/clearotron-ops/COURIER.md +8 -1
  73. package/skills/clearotron-ops/SKILL.md +18 -2
@@ -15,6 +15,10 @@
15
15
 
16
16
  // 6 mandatory store platforms + the general-web cell. Field-scoped cells are additive (a matter can
17
17
  // have MORE rows per variant, never fewer).
18
+ import { readFileSync, existsSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { driverDir } from "../shared/driver-dir.mjs"; // — one definition of where a run's _driver/ is
21
+
18
22
  export const MIN_CELLS_PER_VARIANT = 7;
19
23
 
20
24
  const norm = (s) => (s || "").trim().replace(/^["'`]+|["'`]+$/g, "").toLowerCase();
@@ -281,12 +285,39 @@ export function findGridLedgerViolations(manifestOrTerms, ledgerRaw, { minCellsP
281
285
  // (same contract as findGridLedgerViolations).
282
286
  //
283
287
  // @returns {Array<{variant:string, missing:string[]}>}
284
- export function findPlatformIdentityViolations(manifestOrTerms, ledgerRaw, dictatedPlatforms = []) {
288
+ export function findPlatformIdentityViolations(manifestOrTerms, ledgerRaw, dictatedPlatforms = [], { wholeGrid = false } = {}) {
285
289
  if (!dictatedPlatforms.length) return [];
286
290
  const map = parseGridLedger(ledgerRaw);
287
291
  const want = dictatedPlatforms.map(norm);
288
292
  const out = [];
289
293
  const variants = Array.isArray(manifestOrTerms) ? manifestOrTerms : parseManifestVariants(manifestOrTerms);
294
+
295
+ // ── A CHANNEL NO HALF RAN IS NOBODY'S, AND THAT IS THE ONE THIS COULD NOT SEE ────────────────────
296
+ //
297
+ // The per-variant join below judges a variant that plausibly attempted the whole dictated grid, and
298
+ // skips one whose coverage is partial — deliberately, so the count ladder's carve-outs do not re-fail
299
+ // here. A channel that NO variant reached is partial for every one of them, so every variant was
300
+ // skipped and the channel went unreported.
301
+ //
302
+ // That is what happened on a delivered run. The grid is split in two by term, and both halves carried
303
+ // the same platforms; half A wrote that the repositories were "not in grid mandate, assigned to the
304
+ // parallel half", half B wrote that they were covered "indirectly, via general web search". Neither
305
+ // ran them, each said the other owned them, and the delivered coverage carried it as a NOTE. A pass's
306
+ // statement that another pass owns a channel is not a receipt, and prose is not a state.
307
+ //
308
+ // Reported once, against the whole grid rather than per variant, because the fact is about the
309
+ // channel: nothing in this ledger touched it. The caller fails the fold with it, and the coverage
310
+ // ledger then carries the channel as deferred — open, not run — instead of a sentence.
311
+ const everywhere = new Set();
312
+ for (const cells of map.values()) for (const pl of cells) everywhere.add(pl);
313
+ //
314
+ // ASKED FOR BY THE CALLER, and only where the ledger is the MERGED fold artifact. On one half's own
315
+ // ledger a channel the other half ran is legitimately absent, and reporting it there would re-fail
316
+ // exactly the split this gate was built to allow. The fold is where both halves are in one file, and
317
+ // it is the only place the question "did anybody run this" has an answer.
318
+ const untouched = want.filter((w) => !everywhere.has(w));
319
+ if (wholeGrid && untouched.length && everywhere.size) out.push({ variant: "*", missing: untouched, whole: true });
320
+
290
321
  for (const variant of variants) {
291
322
  // UNION the " / " family's accounted platforms before judging: workers legitimately re-key
292
323
  // between the packed and split forms (copper-conduit, 2026-06-12) and a supplementary closure
@@ -903,3 +934,49 @@ export function findSimilarListingSignals(findingsContent) {
903
934
  flush();
904
935
  return signals;
905
936
  }
937
+
938
+ /**
939
+ * THE CHANNELS THE RUN DICTATED AND NOBODY RAN, as coverage-ledger rows.
940
+ *
941
+ * A channel one half says the other owns is nobody's: on a delivered run half A wrote that the
942
+ * repositories were "not in grid mandate, assigned to the parallel half" and half B wrote that they were
943
+ * covered indirectly through general web search. Neither ran them, and the delivered coverage carried it
944
+ * as a NOTE. Prose is not a state, and a reader cannot act on it.
945
+ *
946
+ * A ROW IN THE LEDGER'S OWN SHAPE AND ITS OWN OPEN STATE, never a new one and never a refusal. `deferred`
947
+ * is what this ledger already calls a slice that could not run at all, every consumer already reads it —
948
+ * the deadline envelope re-runs it, the verdict floor clamps over it, the report shows it where open rows
949
+ * are shown — and a disclosed gap is the point. Failing the fold instead would turn a gap the client can
950
+ * see into a clearance that delivers nothing, which is the opposite of what disclosure is for.
951
+ *
952
+ * Reads the run's own two artifacts and answers `[]` for anything it cannot read: no spec, no merged
953
+ * ledger, or an unparseable one. An absence here is not a finding — the receipts gate above owns that
954
+ * question — and inventing a row from a file this could not read would be a gap nobody can close.
955
+ */
956
+ export function openChannelRows(runDir, io = {}) {
957
+ const read = io.read ?? ((p) => readFileSync(p, "utf8"));
958
+ const exists = io.exists ?? ((p) => existsSync(p));
959
+ // THE SHARED ACCESSOR, not a hand-built path. `shared/driver-dir.mjs` exists to end exactly this:
960
+ // its own header records 1123 hand-built sites across 221 files, and the state that made it
961
+ // indefensible — the hook whose job is policing writes into this subtree computed the subtree's
962
+ // location by hand, like everyone else, so the location was not a decision anybody owned. An arm
963
+ // enforces it; a hand-join here is caught rather than merely untidy.
964
+ const at = (name) => driverDir(runDir, name);
965
+ try {
966
+ if (!exists(at("grid-spec.json")) || !exists(join(runDir, "common-law-grid.json"))) return [];
967
+ const spec = JSON.parse(read(at("grid-spec.json")));
968
+ const platforms = (spec?.platforms ?? []).filter(Boolean);
969
+ if (!platforms.length) return [];
970
+ const ledgerRaw = read(join(runDir, "common-law-grid.json"));
971
+ const terms = (spec?.terms ?? spec?.variants ?? []).filter(Boolean);
972
+ const violations = findPlatformIdentityViolations(terms.length ? terms : [], ledgerRaw, platforms, { wholeGrid: true });
973
+ const whole = violations.find((v) => v.whole);
974
+ return (whole?.missing ?? []).map((platform) => ({
975
+ axis: "common-law",
976
+ status: "deferred",
977
+ unit: `common-law / ${platform}`,
978
+ reason: `open — not run: ${platform} was dictated in the grid and no pass produced a receipt for it. `
979
+ + "A pass stating that another pass owns a channel is not a receipt.",
980
+ }));
981
+ } catch { return []; }
982
+ }
@@ -20,8 +20,8 @@
20
20
  // D3 verify.mjs:1123 checkJson: fail(String(e.message)) — FIVE parsers reach this one site
21
21
  // D4 verify.mjs:1603 parseCoverageLedgerJson, same shape
22
22
  // D5 verify.mjs:1504 fail(`${unaccounted[0].token}:…`) — token minted in a DATA ROW
23
- // D6 verify.mjs:1567 fail(`${violations[0].token}…`) — register-plan.mjs:1395,1399
24
- // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:1996
23
+ // D6 verify.mjs:1567 fail(`${violations[0].token}…`) — validatePlanFeasibility in register-plan.mjs
24
+ // D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2018 disclosureTextByAxis
25
25
  // D8 verify.mjs:1692 fail(caseLawLedgerFail(…)) — token built in case-law-ledger.mjs:204
26
26
  //
27
27
  // A partition built on the 60 tokens a regex CAN see would run green while blind to the rest, which is
@@ -108,10 +108,10 @@ export const VOCABULARY = [
108
108
  { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs:1445" },
109
109
  { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs:1449" },
110
110
  { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
111
- { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1504", family: "driver/register-plan.mjs:1620 (token on a data row)", dynamic: "D5" },
112
- { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs:1567", family: "driver/register-plan.mjs:1395", dynamic: "D6" },
113
- { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs:1567", family: "driver/register-plan.mjs:1744", dynamic: "D6" },
114
- { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs:1558", family: "driver/register-plan.mjs:1996", dynamic: "D7" },
111
+ { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs:1504 coverageFormFail", family: "driver/register-plan.mjs:1642 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
112
+ { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1417 validatePlanFeasibility", dynamic: "D6" },
113
+ { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1766 searchedJurisdictionsFromPlan", dynamic: "D6" },
114
+ { token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2018 disclosureTextByAxis", dynamic: "D7" },
115
115
  { token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs:1578" },
116
116
  { token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
117
117
  { token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs:1603", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
@@ -67,6 +67,34 @@ export function activeElapsedMs({ wall, toolWaitMs = 0, toolAskedAt = null, now
67
67
  // so it can never fire before the byte-stall and never clips a legitimately quiet-but-working stretch
68
68
  // shorter than 5 minutes. A no-progress kill is RECORDED as a stall (signals.stalled + signals.noProgress),
69
69
  // never as "the stage needed more time" — the retry policy must not extend the budget for it.
70
+ // TRICKLE FLOOR — a turn that streams a token every twelve seconds is not a slow turn, and no clock
71
+ // above can see it. The byte-stall resets on any streamed byte; the no-progress ceiling resets on token
72
+ // movement, deliberately, because a healthy streaming turn must never be clipped for being slow. So a
73
+ // trickle held both off and was stopped only by the last-resort wall, at budget + 60s, with the whole
74
+ // attempt discarded and re-run from the start.
75
+ //
76
+ // MEASURED, on one round of one scenario, and that is stated because it is not yet a distribution: two
77
+ // stages killed at the wall streamed 0.08 and 0.12 output tokens per second of ACTIVE time; their own
78
+ // retries, same matter and same engine, ran at 74 and 82.83. Three orders of magnitude apart, which is
79
+ // why a floor needs no fine calibration to separate them — and exactly why the floor below sits far
80
+ // under the slowest healthy sample rather than near the gap. The number that finally ships wants the
81
+ // same window read across the other preserved rounds; until then this is a floor against a pathology,
82
+ // not a budget for a stage.
83
+ //
84
+ // ON ACTIVE TIME, never elapsed, for the reason the hard ceiling states at length: a turn waiting on a
85
+ // slow tool is not producing tokens and must not be killed for it. Zero disables the instrument.
86
+ const minTokensPerSec = () => {
87
+ const v = Number(process.env.CLEAROTRON_MIN_TOKENS_PER_SEC);
88
+ return Number.isFinite(v) && v >= 0 ? v : 1;
89
+ };
90
+ // THE WARM-UP IS NOT A COURTESY, it is what makes the measurement meaningful: a rate over three seconds
91
+ // of active time is noise, and a turn that thinks before it writes would fail a floor applied at once.
92
+ // Five minutes of ACTIVE time is well past the point where a working turn has produced something, and
93
+ // far short of the 46 and 41 minutes the killed attempts burned.
94
+ const rateWarmupMs = () => {
95
+ const v = Number(process.env.CLEAROTRON_MIN_TOKENS_WARMUP_MS);
96
+ return Number.isFinite(v) && v > 0 ? v : 300000;
97
+ };
70
98
  const noProgressMs = (stallClockMs) => {
71
99
  const pinned = Number(process.env.CLEAROTRON_NO_PROGRESS_MS);
72
100
  if (pinned > 0) return pinned;
@@ -418,6 +446,8 @@ export const anthropicAgentEngine = {
418
446
  // (default 120s) so a real wedge still trips fast. Clamp NaN/≤0 → global so a misconfig never disables it.
419
447
  const STALL = (Number(stallSec) > 0 ? Number(stallSec) * 1000 : stallMs());
420
448
  const NOPROG = noProgressMs(STALL); // the honest-progress ceiling (see noProgressMs) — ≥ STALL by construction unless pinned for tests
449
+ const MIN_RATE = minTokensPerSec(); // output tokens per second of ACTIVE time; 0 disables (see minTokensPerSec)
450
+ const WARMUP = rateWarmupMs(); // active time before the floor may fire at all
421
451
  // legacy-engine parity (+60s past the stage timeout); clamp NaN/≤0 → 660s so the hard wall never
422
452
  // silently disables. `CLEAROTRON_HARD_MS` pins it for tests, exactly as CLEAROTRON_STALL_MS and
423
453
  // CLEAROTRON_NO_PROGRESS_MS pin the other two clocks — and it is why this ceiling had no end-to-end arm
@@ -457,6 +487,7 @@ export const anthropicAgentEngine = {
457
487
  try { child.stdin.write(input); child.stdin.end(); } catch { /* child gone — handled by close/error */ }
458
488
  let buf = "", stderr = "", resultEvent = null, killed = false, stallKill = false, settled = false;
459
489
  let noProgressKill = false; // the no-progress ceiling fired (a stall discriminator, see noProgressMs)
490
+ let trickleKill = false; // the token-rate floor fired (a stall discriminator, see minTokensPerSec)
460
491
  let overflow = false; // A3: stdout/stderr exceeded maxBuffer — force a nonzero fail, never parse the truncated tail
461
492
  const maxBuffer = engineMaxBufferChars();
462
493
  let lastMove = Date.now();
@@ -790,6 +821,20 @@ export const anthropicAgentEngine = {
790
821
  if (sig !== null && artifactSeen.get(f) !== sig) { artifactSeen.set(f, sig); progress(); }
791
822
  }
792
823
  };
824
+ // Is this turn producing tokens too slowly to be working? Active time, after the warm-up, over the
825
+ // turn's own streamed output — the same accumulator the journal reports, so the number that kills a
826
+ // turn is the number a reader afterwards sees.
827
+ // What the rate WAS when the floor fired, for the failure line. Two decimals: the readings this
828
+ // exists for are hundredths of a token per second.
829
+ const trickleRate = (now = Date.now()) => {
830
+ const activeMs = activeElapsedMs({ wall: now - (firstOutputAt ?? now), toolWaitMs, toolAskedAt, now });
831
+ return activeMs > 0 ? (((streamedUsage()?.output ?? 0) * 1000) / activeMs).toFixed(2) : "0.00";
832
+ };
833
+ const isTrickling = (now) => {
834
+ const activeMs = activeElapsedMs({ wall: now - firstOutputAt, toolWaitMs, toolAskedAt, now });
835
+ if (activeMs < WARMUP) return false;
836
+ return (streamedUsage()?.output ?? 0) < (MIN_RATE * activeMs) / 1000;
837
+ };
793
838
  const watchdog = setInterval(() => {
794
839
  artifactProgress();
795
840
  const now = Date.now();
@@ -817,6 +862,25 @@ export const anthropicAgentEngine = {
817
862
  // kill it NOW, well below the wall, and record it as a STALL — never let the ceiling raise turn
818
863
  // into extended stall burn, and never let this kill read as "the stage needed more time".
819
864
  else if (progIdle >= (started ? NOPROG : Math.max(NOPROG, GRACE))) { stallKill = true; noProgressKill = true; killed = true; killTree(); }
865
+ // ── A TRICKLE IS A STALL WEARING A STREAM ────────────────────────────────────────────
866
+ //
867
+ // Both clocks above are satisfied by a token every twelve seconds: the byte-stall resets on any
868
+ // byte, and the no-progress ceiling counts token movement as progress ON PURPOSE, because the
869
+ // engine contract promises a slow-but-working streaming turn is never clipped. So the only thing
870
+ // that stopped a trickle was the wall, at budget + 60s, with the whole attempt thrown away.
871
+ //
872
+ // This fires far below the wall and is RECORDED AS A STALL with its own discriminator, so the
873
+ // retry policy treats it as one and never extends the budget for the next attempt — the thing a
874
+ // ceiling raise would have done, and the reason this is not one.
875
+ //
876
+ // The rate is over the turn's own streamed output, on active time, after a warm-up. A healthy
877
+ // turn is nowhere near it: the samples that motivated this ran at 74 and 82.83 tokens per second
878
+ // against kills at 0.08 and 0.12, and the floor sits at 1.
879
+ // ONE CONDITION, NOT A NESTED BLOCK. An `else if (enabled) { if (trickling) … }` would swallow
880
+ // the chain: with the floor enabled and the rate healthy, the hard ceiling below would never be
881
+ // reached, and the only clock that bounds a turn producing tokens at a normal rate forever would
882
+ // have been switched off by adding this one.
883
+ else if (started && MIN_RATE > 0 && isTrickling(now)) { stallKill = true; trickleKill = true; killed = true; killTree(); }
820
884
  // ── THE CEILING MEASURES ACTIVE TIME, NOT ELAPSED ────────────────────────────────────
821
885
  //
822
886
  // Ruling: "there isnt such thing as a hung model. it always delivers something or fails."
@@ -952,6 +1016,13 @@ export const anthropicAgentEngine = {
952
1016
  ? (stderr + `\nrequest timed out (anthropic-agent no-progress watchdog: no token movement / agent-loop step / artifact write for ${Math.round(NOPROG / 1000)}s — a STALL, not a slow turn)`
953
1017
  + `\nanthropic-agent no-progress specimen: firstByteMs=${firstOutputAt === null ? "NEVER" : Math.round(firstOutputAt - t0)}`
954
1018
  + ` toolCalls=${toolCalls} noProgressMs=${NOPROG} graceMs=${GRACE}`)
1019
+ : trickleKill
1020
+ // ITS OWN SENTENCE, because "0 streamed tokens" is the one thing this kill is not. A reader
1021
+ // meeting the stall line over a turn that streamed for forty minutes would go looking for a
1022
+ // silent process and find a talkative one. The rate that killed it rides the line, so the
1023
+ // threshold can be argued from artifacts rather than from memory.
1024
+ ? (stderr + `\nrequest timed out (anthropic-agent trickle floor: ${trickleRate()} output tokens/sec of active time,`
1025
+ + ` under the ${MIN_RATE}/sec floor after a ${Math.round(WARMUP / 1000)}s warm-up — a STALL that kept the pipe warm, not a slow turn)`)
955
1026
  : stallKill ? (stderr + "\nrequest timed out (anthropic-agent stall-watchdog: 0 streamed tokens)")
956
1027
  // Startup-class death (the 3× register-digest code=1 zero-token shape): the CLI exited without
957
1028
  // emitting a single stream event — the failure happened before any turn ran (arg/auth/MCP
@@ -1020,6 +1091,10 @@ export const anthropicAgentEngine = {
1020
1091
  // never journal a token-moving kill as usage:null). noStreamEvents: the CLI died before emitting
1021
1092
  // ANY stream event — a startup-class failure, diagnosable from stderrTail alone.
1022
1093
  signals: { stalled: stallKill || undefined, noProgress: noProgressKill || undefined,
1094
+ // A TRICKLE IS A STALL, and this says which kind. Retry policy keys on `stalled` — so this
1095
+ // never extends the next attempt's budget — while a reader keys on this to tell a turn that
1096
+ // said nothing from one that said almost nothing for three quarters of an hour.
1097
+ trickle: trickleKill || undefined,
1023
1098
  hardWall: (killed && !stallKill) || undefined, rateLimited: rateLimited || undefined, resetsAt,
1024
1099
  usageStreamed: (streamUsage != null) || undefined,
1025
1100
  noStreamEvents: (!sawAnyEvent && resultEvent == null) || undefined,
@@ -114,7 +114,32 @@ export function skeletonPatterns(element) {
114
114
  // also leading/trailing-anchored substrings of the skeleton, so a long mark's family is reachable in pieces.
115
115
  const sk = consonantSkeleton(w);
116
116
  if (sk.length >= 2) { out.add(`${sk[0]}*${sk[sk.length - 1]}`); }
117
- return [...out];
117
+ // ── A PATTERN THAT DEGENERATES TO ITS OWN ELEMENT IS NOT A PATTERN ────────────────────────────────
118
+ //
119
+ // The vowel-slot loop above replaces each vowel RUN with `?`. An element carrying no vowel has no run
120
+ // to replace, so it falls through the loop unchanged and `pat` is the normalized element itself — a
121
+ // plain string with no pattern syntax in it. Every initialism is in this class: SMS, BCG, KFC, HSBC,
122
+ // MTV, CNN, and single-character elements like X.
123
+ //
124
+ // That string used to be returned and dispatched under the hard-coded `wildcard` predicate the
125
+ // register-plan fringe pushes. `termPredicateIssue` reads the pair as unexecutable, the pipeline
126
+ // raises StageFailure at plan compile because the entry was freshly minted rather than inherited, and
127
+ // THE WHOLE MATTER DIES BEFORE ONE QUERY IS SENT — nothing delivered, and the message correctly says
128
+ // retrying will not help. A mark whose dominant element has no vowel could not be cleared at all.
129
+ //
130
+ // SCREENED HERE RATHER THAN AT THE PUSH SITE, and that is a decision rather than convenience. The
131
+ // entry is worthless, not a lost slice: a starless wildcard term maps to `wildcardInfix`, which every
132
+ // provider serves as a contains search over the raw term — the same search the crowd-gate parent
133
+ // already dispatches one line earlier with `predicate: "default"` over the same element. Stamping it
134
+ // `unsupported` at the funnel would therefore publish a disclosed coverage gap for an axis that is
135
+ // NOT uncovered, and a false gap in a client's report is worse than the honest death it replaces.
136
+ // The funnel's own rules (markup, substance, variant values) screen terms the compiler does not
137
+ // author; this one it authors, here, and this is the only producer of `wildcardPatterns` in the tree.
138
+ //
139
+ // ONE SCREEN OVER EVERYTHING THIS FUNCTION RETURNS, not a guard beside each member. The anchored
140
+ // form below carries `*` by construction today, so filtering it is a no-op today — which is the
141
+ // point: a pattern added here later is covered without anybody remembering this rule.
142
+ return [...out].filter((p) => /[*?]/.test(p));
118
143
  }
119
144
 
120
145
  // ── Visual confusables (Unicode-confusable axis) — look-alikes an examiner/consumer would conflate ──────
@@ -241,7 +266,24 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
241
266
  dropped_axes: [...drop].sort(),
242
267
  axes: [
243
268
  { axis: "edit-1", count: edits.length, mechanism: "Damerau-Levenshtein edit-1, exhaustive" },
244
- { axis: "phonetic-family", count: wildcards.length, mechanism: drop.has("phonetic-family") ? "DROPPED — judgment's variant-layer scope decision" : `consonant-skeleton wildcard + Double-Metaphone key(s) [${keys.join(",")}]` },
269
+ // NO PATTERN is a THIRD state, and it is disclosed in the same voice as a judgment drop. An
270
+ // element with too few consonants to anchor a skeleton wildcard (X, and anything normalizing to
271
+ // one character) yields no retrieval pattern at all — see skeletonPatterns. Silence here would
272
+ // let the axis read as ordinary, when what happened is that it contributed nothing.
273
+ //
274
+ // AND THE KEYS ARE A SEPARATE QUESTION FROM THE PATTERNS, so the row asks it separately. For an
275
+ // element like X the metaphone keys survive and really do verify which returned marks are true
276
+ // sound-alikes, and the row should say so rather than call the whole axis dead. For a purely
277
+ // numeric element — "99", "5" — there are no keys either, and a row that still claimed the keys
278
+ // verify would be a CLIENT-FACING CLAIM OF A VERIFICATION THAT DID NOT HAPPEN. That is the worse
279
+ // failure of the two, so the clause is dropped rather than printed empty. The first draft of
280
+ // this row hard-coded the clause and pinned only X in its test, which has keys — the arm named
281
+ // the disclosure property and drove one member of it, so it passed while this was live.
282
+ { axis: "phonetic-family", count: wildcards.length,
283
+ mechanism: drop.has("phonetic-family") ? "DROPPED — judgment's variant-layer scope decision"
284
+ : wildcards.length ? `consonant-skeleton wildcard + Double-Metaphone key(s) [${keys.join(",")}]`
285
+ : keys.length ? `NO PATTERN — "${el}" has too few consonants to anchor a skeleton wildcard, so the family has no retrieval pattern; Double-Metaphone key(s) [${keys.join(",")}] still verify what the other axes return`
286
+ : `NO PATTERN — "${el}" has too few consonants to anchor a skeleton wildcard and yields no Double-Metaphone key, so this axis contributes nothing for this element and verifies nothing` },
245
287
  { axis: "visual-confusable", count: confs.length, mechanism: drop.has("visual-confusable") ? "DROPPED — judgment's variant-layer scope decision" : "Unicode-confusable homoglyph + multigraph table" },
246
288
  { axis: "transliteration", count: trans.length, mechanism: drop.has("transliteration") ? "DROPPED — judgment's variant-layer scope decision" : `scoped scripts: ${scripts.join(", ")}` },
247
289
  ],
@@ -125,6 +125,16 @@ export function renderMatterFrame(model) {
125
125
  out.push(`- **Scope jurisdictions:** ${model.scope_jurisdictions.join(", ")}`);
126
126
  if (model.excluded_jurisdictions.length)
127
127
  out.push(`- **Excluded jurisdictions:** ${model.excluded_jurisdictions.join(", ")}`);
128
+ // CLASSES THE FRAME ADDED, WITH THE REASON IT ADDED THEM. Rendered as its own rows rather than folded
129
+ // into the instructed scope above, because the two are different claims: that section is what the
130
+ // client asked for, quoted from the driver's intake record, and these are what the frame concluded is
131
+ // necessary as well. A reader who cannot tell those apart cannot tell an instruction from a judgement.
132
+ //
133
+ // NOTHING RENDERS WHEN THERE ARE NONE — no heading, no "none" line. An asserted zero is right where a
134
+ // seat might have skipped the question (meaning angles below), and wrong here: the frame is not asked
135
+ // to find extra classes, so finding none is the ordinary case and not an answer worth a row.
136
+ for (const c of (model.identified_classes ?? []))
137
+ out.push(`- **Class ${c.class} — identified by the frame:** ${c.reason}`);
128
138
  out.push("");
129
139
 
130
140
  // `Search channels:` — domains only; the grid site-restricts to them and the general web is always
@@ -156,13 +166,34 @@ export function renderMatterFrame(model) {
156
166
  */
157
167
  /** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
158
168
  const DECLARED = Object.freeze({
159
- "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks"],
169
+ "": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes"],
160
170
  intake_asks: ["ask", "owner"],
171
+ identified_classes: ["class", "reason"],
161
172
  });
162
173
 
163
174
  /** Refuse an undeclared key by path, at depth. Shared walk; the table above is what is this tool's. */
164
175
  export const refuseUndeclared = (params) => refuseUndeclaredShared(params, DECLARED, "matterframe");
165
176
 
177
+ /**
178
+ * The Nice classes the frame judged necessary beyond the instructed ones, as strings. IMPURE (reads
179
+ * the run's own accepted call).
180
+ *
181
+ * THE PLAN COMPILE UNIONS THIS WITH THE INSTRUCTED CLASSES so every variant axis carries both. Before
182
+ * it existed the axes carried the instructed classes alone and a class the frame had identified reached
183
+ * the sweep only if something proposed it as supplemental work — where it competed for capped slots
184
+ * with model-minted extras. A cap was deciding coverage the frame had already judged necessary, and on
185
+ * the run this came from the delivered report said two such classes were "covered for the name and open
186
+ * for its variants" while the reviewing lawyer's scope included one of them throughout.
187
+ *
188
+ * EMPTY IS THE ORDINARY ANSWER and must stay cheap: no frame yet, a legacy or replayed run whose
189
+ * accepted call predates the field, a frame that identified nothing — all of them return `[]`, the
190
+ * union is a no-op, and the plan is exactly what it was.
191
+ */
192
+ export function frameIdentifiedClasses(runDir) {
193
+ const rows = lastAcceptedMatterFrame(runDir)?.identified_classes;
194
+ return (Array.isArray(rows) ? rows : []).map((r) => String(r?.class ?? "").trim()).filter(Boolean);
195
+ }
196
+
166
197
  /** The last ACCEPTED call for this run, or null. */
167
198
  export function lastAcceptedMatterFrame(runDir) {
168
199
  return lastAccepted(matterFrameCallPaths(String(runDir ?? "")).accepted, readFileSync);
@@ -190,6 +221,12 @@ export function mergeMatterFrameCall(stored, received) {
190
221
  scope_jurisdictions: keepIfAbsent(received?.scope_jurisdictions, base.scope_jurisdictions),
191
222
  excluded_jurisdictions: keepIfAbsent(received?.excluded_jurisdictions, base.excluded_jurisdictions),
192
223
  meaning_angles_none: keepIfAbsent(received?.meaning_angles_none, base.meaning_angles_none),
224
+ // KEEP-IF-ABSENT, for the same reason as the two above and one more. These are classes the frame
225
+ // judged necessary beyond the instructed ones, and the plan compile unions them into every variant
226
+ // axis — so a partial call that dropped them would not merely make the frame quieter, it would
227
+ // NARROW THE SEARCH on the next compile, silently and in the direction that misses rights. An
228
+ // omission here is a repair that did not mention them, never a decision to withdraw them.
229
+ identified_classes: keepIfAbsent(received?.identified_classes, base.identified_classes),
193
230
  };
194
231
  }
195
232
 
@@ -228,6 +265,35 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
228
265
  intake_asks.push({ ask, owner });
229
266
  }
230
267
 
268
+ // ── CLASSES THE FRAME JUDGED NECESSARY BEYOND THE INSTRUCTED ONES ─────────────────────────────────
269
+ //
270
+ // The frame reads the description of use and can conclude that a class nobody instructed is in scope.
271
+ // It has always said so in its prose. Nothing could act on that: the plan compile takes its classes
272
+ // from the driver's intake record, so the identified ones reached the sweep only if a model proposed
273
+ // them as supplemental work, where they competed for capped slots with model-minted extras — a cap
274
+ // deciding coverage the frame had already judged necessary.
275
+ //
276
+ // TYPED RATHER THAN PARSED OUT OF THE PROSE, and that is the whole reason this field exists. Deriving
277
+ // classes from judgment prose is ruled against with measurement: 19 of 21 runs carry the same class
278
+ // number in both an applied and a dropped row, and deriving there dropped the PRIMARY class. A field
279
+ // the frame fills in deliberately is a decision; a number scraped out of a sentence is a guess.
280
+ //
281
+ // A REASON PER CLASS IS REQUIRED, not decorative. This widens a client's search, and the next reader
282
+ // asking why class 9 was swept needs the frame's own sentence rather than an inference from a number.
283
+ // ABSENT OR EMPTY CHANGES NOTHING — the compile unions an empty list and the plan is what it was.
284
+ const identified_classes = [];
285
+ for (const c of (Array.isArray(params?.identified_classes) ? params.identified_classes : [])) {
286
+ const raw = str(c?.class), reason = str(c?.reason);
287
+ const n = Number(raw);
288
+ if (!Number.isInteger(n) || n < 1 || n > 45)
289
+ return { ok: false, reason: `matterframe_identified_class_invalid:${raw || "<empty>"} — a Nice class is a whole number 1-45` };
290
+ if (!reason)
291
+ return { ok: false, reason: `matterframe_identified_class_reason_missing:${n} — every class the frame adds carries a one-line reason, because it widens what the client is charged to search` };
292
+ if (identified_classes.some((k) => k.class === String(n)))
293
+ return { ok: false, reason: `matterframe_identified_class_duplicate:${n} — one row per class` };
294
+ identified_classes.push({ class: String(n), reason });
295
+ }
296
+
231
297
  const model = {
232
298
  schema_version: SCHEMA_VERSION,
233
299
  instructed_scope: instructedScope ?? null,
@@ -238,6 +304,7 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
238
304
  search_channels: list(params?.search_channels),
239
305
  meaning_angles, meaning_angles_none,
240
306
  intake_asks,
307
+ identified_classes,
241
308
  };
242
309
  return { ok: true, model, content: renderMatterFrame(model) };
243
310
  }
@@ -137,7 +137,7 @@ export function parseNamedBand(raw) {
137
137
  // byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
138
138
  // run: four capability-gap blocks carried `error:true, deferred:true` into this function and
139
139
  // reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
140
- // COUNT for this slice". register-plan.mjs:1417 already enforces the same rule one layer up
140
+ // COUNT for this slice". register-plan.mjs:1439 validatePlanFeasibility already enforces the same rule one layer up
141
141
  // ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
142
142
  // RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
143
143
  // Conditional like the four keys above, so old bands carry neither key and nothing shifts.
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.1-beta.0",
5
+ "version": "0.3.1-beta.2",
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": {
@@ -1146,7 +1146,19 @@ export async function knockoutInner(ctx, job, opts = {}) {
1146
1146
  // because mark_sent is the only clear and it settles a failure.json on the same evidence a delivery
1147
1147
  // needs. The marker is NOT duplicated — the primary lane's own packet lands `<runId>.failed.pending`
1148
1148
  // and the *.pending watch matches it; a second `<runId>.pending` would outlive every settle.
1149
+ // THE RECORD OF AN OBLIGATION MUST NOT BE CONTINGENT ON THE ACTION IT OBLIGES — clearance parity,
1150
+ // and the same defect this lane would have had. `writeOutboxPacket` returns null rather than throwing,
1151
+ // so an unwritable outbox reaches the raw marker write below; with the flag after it, that throw took
1152
+ // the record of the obligation with it and the run ended failed and NOT owed. Guards first, then the
1153
+ // flag, then the writes that can fail.
1149
1154
  let failPingSent = false;
1155
+ try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
1156
+ try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
1157
+ // a fresh notice supersedes an older send's skip-guards (a resumed-then-failed-again run must
1158
+ // still notify — the .sent/receipts invariant, clearance parity)
1159
+ try { writeRunStatus(ctx, { sendPending: true }); } catch (sErr) {
1160
+ note(`knockout failure notice could NOT be recorded as owed (${String(sErr?.message ?? sErr).slice(0, 100)}) — the run directory is unwritable`);
1161
+ }
1150
1162
  try {
1151
1163
  const rich = buildFailurePacket({
1152
1164
  runId, agent, job, failedStage, shortReason, terminalKind,
@@ -1163,12 +1175,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
1163
1175
  mkdirSync(config.outboxDir, { recursive: true });
1164
1176
  writeFileSync(join(config.outboxDir, `${runId}.pending`), `${agent}\n`);
1165
1177
  }
1166
- // a fresh notice supersedes an older send's skip-guards (a resumed-then-failed-again run must
1167
- // still notify — the .sent/receipts invariant, clearance parity)
1168
- try { rmSync(join(run.runDir, ".sent")); } catch { /* none */ }
1169
- try { rmSync(driverDir(run.runDir, "send-receipts.json"), { force: true }); } catch { /* none */ }
1170
- writeRunStatus(ctx, { sendPending: true });
1171
- } catch (nfErr) { note(`knockout failure-notice write skipped (${String(nfErr?.message ?? nfErr).slice(0, 100)})`); }
1178
+ } catch (nfErr) { note(`knockout failure notice is owed but its packet/marker could not be written (${String(nfErr?.message ?? nfErr).slice(0, 100)}) — status.json carries the obligation`); }
1172
1179
  note(`=== KNOCKOUT ${terminalKind === REFUSAL_TERMINAL_KIND ? "REFUSED" : "FAILED"} ${run.codename} at ${failedStage}: ${shortReason} ===\n`);
1173
1180
  // `codename`: this lane's terminals reach the SAME CLI exit as the clearance lane's, and the
1174
1181
  // CLI composes its resume line from the returned identity. Without it a knockout operator is the only