clearotron 0.3.2 → 0.3.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +109 -13
  4. package/bin/start.mjs +1 -1
  5. package/build-info.json +2 -2
  6. package/demo/MANIFEST.json +27 -0
  7. package/docs/INTAKE.md +8 -0
  8. package/docs/architecture/04-configuration-reference.md +29 -11
  9. package/driver/CHANGELOG.md +49 -0
  10. package/driver/citation-census.json +3 -3
  11. package/driver/clearance-variants-record.mjs +12 -1
  12. package/driver/common-law-coverage-status.mjs +113 -0
  13. package/driver/config-inventory.mjs +1 -1
  14. package/driver/contract-audit.mjs +1 -1
  15. package/driver/contract-e3-backlog.mjs +37 -37
  16. package/driver/contract-vocabulary.mjs +8 -8
  17. package/driver/coverage-form-io.mjs +3 -1
  18. package/driver/coverage-form.mjs +38 -11
  19. package/driver/coverage-ledger.mjs +37 -7
  20. package/driver/coverage-union.mjs +2 -2
  21. package/driver/crowd-context.mjs +19 -6
  22. package/driver/dev-portal.mjs +3 -3
  23. package/driver/drainer-identity.mjs +1 -1
  24. package/driver/driver.config.mjs +80 -9
  25. package/driver/engine/CONTRACT.md +3 -2
  26. package/driver/engine/anthropic-agent.mjs +34 -7
  27. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  28. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  29. package/driver/engine/mcp/coverage-server.mjs +1 -1
  30. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  31. package/driver/engine/mcp/euipo-server.mjs +2 -0
  32. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  33. package/driver/engine/mcp/gather-config.mjs +8 -2
  34. package/driver/engine/mcp/probe-server.mjs +37 -0
  35. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  36. package/driver/engine/mcp/recording-server.mjs +30 -0
  37. package/driver/engine/mcp/signa-server.mjs +2 -0
  38. package/driver/engine/mcp/supplemental.mjs +89 -12
  39. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  40. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  41. package/driver/engine/openai-agent.mjs +7 -0
  42. package/driver/engine/probe.mjs +67 -14
  43. package/driver/engine/tool-refusal.mjs +16 -0
  44. package/driver/enqueue-schema.mjs +2 -2
  45. package/driver/envelope-settle.mjs +82 -13
  46. package/driver/findings-model.mjs +4 -4
  47. package/driver/gateway.mjs +18 -2
  48. package/driver/manager-groups-verdict.mjs +1 -1
  49. package/driver/matter-frame-record.mjs +24 -7
  50. package/driver/named-band.mjs +1 -1
  51. package/driver/package.json +1 -1
  52. package/driver/partial-payload-baseline.json +12 -3
  53. package/driver/pipeline-knockout.mjs +3 -3
  54. package/driver/pipeline.mjs +154 -50
  55. package/driver/plan-run-agreement-verdict.mjs +49 -0
  56. package/driver/portal-service.mjs +8 -4
  57. package/driver/progress.mjs +14 -3
  58. package/driver/publish/index.mjs +41 -26
  59. package/driver/publish/report-data.mjs +4 -3
  60. package/driver/publish/xlsx.mjs +26 -4
  61. package/driver/queue-markers.mjs +44 -0
  62. package/driver/queue-watch-verdict.mjs +2 -2
  63. package/driver/reference-score.mjs +10 -2
  64. package/driver/register-availability.mjs +2 -2
  65. package/driver/register-plan.mjs +313 -21
  66. package/driver/roster-verdict.mjs +1 -1
  67. package/driver/runner.mjs +26 -2
  68. package/driver/settle-stamp.mjs +10 -3
  69. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  70. package/driver/skills/clearance-register/SKILL.md +44 -3
  71. package/driver/skills/clearance-register/digest.md +5 -5
  72. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  73. package/driver/skills/clearance-register/unit.md +39 -0
  74. package/driver/skills/clearance-variants/SKILL.md +1 -1
  75. package/driver/skills/matter-frame/SKILL.md +4 -2
  76. package/driver/stages.mjs +12 -5
  77. package/driver/status-snapshot.mjs +2 -2
  78. package/driver/suite-census.json +293 -29
  79. package/driver/synthesis-record.mjs +80 -2
  80. package/driver/unit-file-drift.mjs +3 -3
  81. package/driver/unit-inventory.mjs +2 -2
  82. package/driver/unit-state-verdict.mjs +1 -1
  83. package/driver/updater-identity.mjs +2 -3
  84. package/driver/variant-manifest-model.mjs +11 -1
  85. package/driver/verify.mjs +5 -5
  86. package/driver/withheld-families.mjs +104 -0
  87. package/mcp-server/CHANGELOG.md +8 -0
  88. package/mcp-server/lib/brief.mjs +16 -12
  89. package/mcp-server/lib/runs.mjs +1 -1
  90. package/mcp-server/package.json +1 -1
  91. package/mcp-server/server.mjs +3 -2
  92. package/package.json +2 -2
  93. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  94. package/portal-ui/dist/index.html +1 -1
  95. package/portal-ui/package.json +1 -1
  96. package/providers/_shared/count.mjs +2 -2
  97. package/providers/_shared/enumerate.mjs +15 -2
  98. package/providers/_shared/execute-plan.mjs +19 -1
  99. package/providers/_shared/plan-guards.mjs +40 -0
  100. package/providers/clarivate/src/capabilities.js +15 -5
  101. package/providers/clarivate/src/core.js +41 -5
  102. package/providers/corsearch/src/capabilities.js +4 -0
  103. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  104. package/providers/oauth-mcp-bridge/package.json +1 -1
  105. package/providers/signa/src/capabilities.js +22 -8
  106. package/providers/signa/src/core.js +12 -1
  107. package/scripts/demo-evidence.mjs +114 -0
  108. package/scripts/e2e.mjs +1 -1
  109. package/scripts/engine-probe.mjs +6 -5
  110. package/scripts/env-audit.mjs +1 -1
  111. package/scripts/freeze-example-run.mjs +3 -3
  112. package/scripts/live-surface-check.mjs +32 -33
  113. package/scripts/mint-suite-census.mjs +66 -0
  114. package/scripts/package-size-budget.mjs +117 -0
  115. package/scripts/register-plan-shape.mjs +259 -0
  116. package/scripts/release-note-required.mjs +38 -1
  117. package/scripts/repo-writes.mjs +1 -1
  118. package/scripts/report-sections-render-check.mjs +7 -3
  119. package/scripts/score.mjs +7 -1
  120. package/scripts/settings-render-check.mjs +36 -0
  121. package/scripts/travelling-predicates.mjs +1 -1
  122. package/shared/identifier-scan.mjs +22 -5
  123. package/shared/scroll-settle.mjs +67 -0
@@ -10,6 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
10
10
  import { createHash } from "node:crypto";
11
11
  import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
12
12
  import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
13
+ import { goodsOf } from "./queue-markers.mjs"; // — one reading of "does this job name goods", shared with the intake gate
13
14
  import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
14
15
  import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
15
16
  import { fileURLToPath } from "node:url";
@@ -39,8 +40,8 @@ import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections
39
40
  import { readAcceptedFlags } from "./narrative-refutation-record.mjs"; // T3b — the typed flags, not the re-parse
40
41
  import { evidenceClaimViolations, evidenceClaimTable } from "./evidence-claim-invariant.mjs"; //
41
42
  import { buildCorrectionsApplied, correctionsWorklist, correctionsAppliedTable, correctionScope, scopeDrift, unresolvedFlags, reportLines, linesOf, REPORT_LINE_KEY, REPORT_LINE_LABEL } from "./corrections-feedforward.mjs";
42
- import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
43
- import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection } from "./envelope-settle.mjs";
43
+ import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, formRowUnitKey, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
44
+ import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection, readStickyGaps } from "./envelope-settle.mjs";
44
45
  import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
45
46
  import { parseNamedBand, mergeNamedBands, findCollapsedBands, quarantineUnknownStates, taintQuarantineCleanBlocks, bandRecords } from "./named-band.mjs";
46
47
  import { recordOriginsFor } from "./record-origins.mjs";
@@ -90,14 +91,14 @@ import { deriveScopeFacts } from "./scope-facts.mjs";
90
91
  import { documentGrowth } from "./gate-metrics.mjs";
91
92
  import { editRepairTail, abbrev } from "./repair-contract.mjs";
92
93
  import { repairFollowup } from "./repair-composers.mjs";
93
- import { seedRunStatus, recordTransition, writeRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState } from "./progress.mjs";
94
+ import { seedRunStatus, recordTransition, writeRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState, signoffPatch, readSignoff } from "./progress.mjs";
94
95
  import { batchMarkName } from "./mark-name.mjs";
95
96
  import { writeOutboxPacket } from "./outbox.mjs";
96
97
  import { publishReport, composeEmailHtml, deliverySubject } from "./publish/index.mjs";
97
98
  import { parseCaseLawProfiles, joinCaseLawProfiles } from "./publish/parse.mjs";
98
99
  import { buildAuditMd, parseSpineFindingBlocks } from "./publish/audit-from-spine.mjs";
99
100
  import { deriveRegisterPresence } from "./publish/register-presence.mjs"; // — the audit stores every live in-scope record
100
- import { lastAcceptedMatterFrame, frameIdentifiedClasses, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, which the plan compile unions in
101
+ import { lastAcceptedMatterFrame, frameIdentifiedClasses, frameIdentifiedClassRows, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, each with its reason, which the plan compile gives one identical-mark question apiece (decision 18)
101
102
  import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs";
102
103
  import { excludeHouseElement, verifyHouseElementOwnership, resolveRegions as resolvePlanRegions, HOUSE_ELEMENT_RECEIPT } from "./register-plan.mjs"; // 647 — the client's own element leaves the conflict analysis only on a verified receipt
103
104
  import { resolveRecordExecutor } from "./register-records.mjs"; // — the stamp the late lanes never met
@@ -463,8 +464,35 @@ export function readPlanExecution(ctx) { // @internal
463
464
  const p = ctx?.paths?.planExecution;
464
465
  try { return p && existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null; } catch { return null; }
465
466
  }
467
+ /**
468
+ * A qid this run has accepted as a capability gap (envelope-settle.mjs stickyGapsAfter) leaves `missing`
469
+ * for `deferred`, with the reason it was accepted under, so no re-join can put it back on the ladder.
470
+ * Returns the join unchanged when nothing is held. PURE.
471
+ */
472
+ export function holdStickyGapsIn(join, sticky) { // @internal
473
+ const held = (join?.missing ?? []).filter((q) => sticky?.has(q));
474
+ if (!held.length) return join;
475
+ return { ...join, missing: join.missing.filter((q) => !sticky.has(q)),
476
+ deferred: [...(join.deferred ?? []), ...held.map((q) => ({ qid: q, reason: String(sticky.get(q)?.reason ?? "").slice(0, 300) }))] };
477
+ }
478
+
479
+ /**
480
+ * The coverage units of this run's sticky capability gaps, as ledgerUnitKey()s, found through the coverage
481
+ * form's own deferred rows (the driver writes each with its qid). The envelope, the escalation and the
482
+ * skeptic's ledger split then hold those rows whatever reason the seat wrote for them. A run with no form,
483
+ * or no sticky gap, holds nothing extra, exactly as before.
484
+ */
485
+ export function stickyGapUnits(P) { // @internal
486
+ const sticky = readStickyGaps(P);
487
+ if (!sticky.size) return new Set();
488
+ const stamp = coverageFormStamp(P.runDir);
489
+ const rows = stamp.required ? (readCoverageForm(P.runDir, stamp.formName).rows ?? []) : [];
490
+ return new Set(rows.filter((r) => r?.kind === "deferred" && sticky.has(String(r.qid ?? ""))).map(formRowUnitKey));
491
+ }
492
+
466
493
  export function writePlanExecutionReceipt(ctx, joinRes) { // @internal
467
494
  const P = ctx.paths;
495
+ joinRes = holdStickyGapsIn(joinRes, readStickyGaps(P));
468
496
  // — the ONE place the receipt is written is the one place this decision is taken. The
469
497
  // reclassification is keyed off the receipt already on disk (ladderExhaustedQids), so every writer
470
498
  // agrees without any of them knowing about it: the fan-in, the envelope's re-join after its close
@@ -2384,7 +2412,12 @@ function attachRegisterPlan(ctx, { frozenOnly = false } = {}) {
2384
2412
  // for the documented normal case. Harmless on corsearch (an absent region clause is a worldwide
2385
2413
  // sweep); fatal on a provider whose regions[] is mandatory, where every entry then errored on its
2386
2414
  // count probe and the whole plan joined MISSING at fan-in (review finding 11).
2387
- job: { jobKey: ctx.run.slug, classes: [...new Set([...inScopeClassList(ctx.job, ctx.profile).map(String), ...frameIdentifiedClasses(P.runDir)])], jurisdictions: registerJurisdictions(ctx.job, ctx.profile) },
2415
+ // DECISION 18: the frame's added classes are no longer UNIONED into the plan's class scope. They
2416
+ // ride one identical-mark entry each instead, which is what an added class is supposed to cost —
2417
+ // unioning put every added class on every variant and every family. The instructed scope is what
2418
+ // it always was, and `addedClasses` only ever appends.
2419
+ job: { jobKey: ctx.run.slug, classes: inScopeClassList(ctx.job, ctx.profile).map(String), jurisdictions: registerJurisdictions(ctx.job, ctx.profile) },
2420
+ addedClasses: frameIdentifiedClassRows(P.runDir),
2388
2421
  form, skillVersion: "clearance-register@spec48",
2389
2422
  // — WHICH ELEMENT THE EXCLUSION TOOK OUT, so the compile can make its form band unreachable
2390
2423
  // rather than merely unasked-for. Null unless the ownership receipt verified, which is the same
@@ -6219,10 +6252,11 @@ export function skepticDeferralExtra(ctx) { // @internal
6219
6252
  const gapAxes = capabilityGapAxes(ctx.registerPlan, receipt);
6220
6253
  const fullyDeferred = new Set((ctx.registerPlan ? fullyDeferredAxes(ctx.registerPlan) : []).map((a) => String(a.axis).toLowerCase()));
6221
6254
  const axes = [...new Set(rows.map((r) => String(r.axis ?? "").toLowerCase()).filter(Boolean))];
6255
+ const heldUnits = stickyGapUnits(P);
6222
6256
  const ledgerLines = coverageLedgerTableRows(rows);
6223
6257
  const held = [], closeable = [];
6224
6258
  for (const a of axes) {
6225
- const s = splitDeferredByCloseability(rows, a, gapAxes, { fullyDeferred: fullyDeferred.has(a) });
6259
+ const s = splitDeferredByCloseability(rows, a, gapAxes, { fullyDeferred: fullyDeferred.has(a), heldUnits });
6226
6260
  for (const r of s.held) held.push(`${a} / ${r.unit} — ${String(r.reason ?? "").replace(/\s+/g, " ").slice(0, 160)}`);
6227
6261
  for (const r of s.closeable) closeable.push(`${a} / ${r.unit} — ${String(r.reason ?? "").replace(/\s+/g, " ").slice(0, 160)}`);
6228
6262
  }
@@ -6230,6 +6264,16 @@ export function skepticDeferralExtra(ctx) { // @internal
6230
6264
  const deferredQids = deferredList.slice(0, 24)
6231
6265
  .map((d) => `- ${d.qid} — ${String(d.reason ?? "").replace(/\s+/g, " ").slice(0, 200)}`);
6232
6266
  const more = deferredList.length > 24 ? [`- …and ${deferredList.length - 24} more (read ${P.planExecution} for the rest)`] : [];
6267
+ // THE CLASSES THE FRAME ADDED, AND WHAT EACH RETURNED. This block names what was refused and what is
6268
+ // open, and nothing that ran, so a class the frame added beyond the instructed ones — asked and
6269
+ // answered with its records listed — was absent from it, and the skeptic reported it as never swept.
6270
+ const addedClassRows = (ctx.registerPlan?.entries ?? []).filter((e) => e?.added_class_reason).map((e) => {
6271
+ const ran = (receipt?.executed ?? []).find((x) => x.qid === e.qid);
6272
+ const answer = ran ? `${ran.state}${Number.isFinite(ran.records) ? `, ${ran.records} records` : ""}`
6273
+ : (receipt?.deferred ?? []).some((d) => d.qid === e.qid) ? "refused (listed above)"
6274
+ : (receipt?.missing ?? []).includes(e.qid) ? "not run (missing)" : "not in the receipt";
6275
+ return `- ${e.qid} (class ${(e.nice_classes ?? []).join(", ")}) — ${answer}`;
6276
+ });
6233
6277
  const ownerNegative = ownerScreenNegative(readOwnerScreen(P));
6234
6278
  return lines(
6235
6279
  `COVERAGE + EXECUTION, DRIVER-COMPUTED — do NOT re-derive any of this from the findings prose. These rows come from ${P.registerCoverageLedger} and ${P.planExecution}, the machine artifacts the driver wrote; both are also yours to read directly, but the answer to "what is still open, and can a re-run close it" is already below.`,
@@ -6240,6 +6284,7 @@ export function skepticDeferralExtra(ctx) { // @internal
6240
6284
  deferredQids.length
6241
6285
  ? lines(`Plan-execution receipt — queries the ACTIVE PROVIDER REFUSED deterministically (${deferredList.length} of ${(receipt.executed?.length ?? 0) + deferredList.length + (receipt.missing?.length ?? 0)} planned), with the mechanical reason per query:`, ...deferredQids, ...more)
6242
6286
  : "Plan-execution receipt: no query was deterministically refused by the provider this run.",
6287
+ addedClassRows.length ? lines("", "Classes the frame added beyond the instructed ones — each question, and what it returned:", ...addedClassRows) : "",
6243
6288
  "",
6244
6289
  closeable.length
6245
6290
  ? `CLOSEABLE floor obligations (a warm re-run reaches these — escalate them if they are material): ${closeable.join("; ")}.`
@@ -6401,7 +6446,7 @@ function plainRegisterExtra(ctx) {
6401
6446
  }
6402
6447
  }
6403
6448
 
6404
- // ── THE RECEIPT, AND THE THREE CLASSES, STATED ONCE ────────────────────────────────────────────────
6449
+ // ── THE RECEIPT, AND THE GRADED CLASSES, STATED ONCE ────────────────────────────────────────────────
6405
6450
  //
6406
6451
  //. The receipt this block tabulates is the answer to "did that search run", and until now exactly
6407
6452
  // one stage got it: the REVIEWER. `synthesis` — the stage that writes the claim the reviewer then
@@ -6419,7 +6464,7 @@ function plainRegisterExtra(ctx) {
6419
6464
  // out twice, once per seat, is the shape — one rule in two places, drifting from the day the second
6420
6465
  // copy is typed — and the classes are the part that must never drift, because class (1) is the blocking
6421
6466
  // condition.
6422
- // THE HEAD AND THE THREE CLASSES ARE NOT DECLARED HERE. They live in register-plan.mjs, beside the
6467
+ // THE HEAD AND THE GRADED CLASSES ARE NOT DECLARED HERE. They live in register-plan.mjs, beside the
6423
6468
  // derivation that assigns the states, because the gateway's corrective hint is a THIRD reader of the
6424
6469
  // same grading and `pipeline.mjs` imports `gateway.mjs` — so a constant declared here could never
6425
6470
  // reach it.
@@ -6447,8 +6492,13 @@ function planAuditExtra(ctx, { stage = "narrative-refutation" } = {}) {
6447
6492
  `- executed: ${exec.executed.length} entr${exec.executed.length === 1 ? "y" : "ies"} (${crowds.length} crowd/incomplete${crowds.length ? `: ${crowds.slice(0, 4).map((x) => x.qid).join("; ")}` : ""})`,
6448
6493
  `- missing (no band block): ${exec.missing.length}${exec.missing.length ? ` — ${exec.missing.slice(0, 4).join("; ")}` : ""}`,
6449
6494
  `- skipped (crowd-gated fringe): ${exec.skipped.length}`,
6495
+ // The families waiting for the reading turn, and those it asked. Without these lines the table's
6496
+ // own buckets summed to the whole plan less the waiting families, and a reviewer read it as
6497
+ // "no family waiting" while the receipt held 156.
6498
+ `- awaiting the reading turn's ask: ${exec.awaiting?.length ?? 0}`,
6499
+ exec.asked?.length ? `- asked by the reading turn (a waiting family's question, asked by another entry): ${exec.asked.length}` : "",
6450
6500
  exec.unplanned?.length ? `- unplanned qid-stamped blocks: ${exec.unplanned.length}` : "",
6451
- ...(exec.skeleton ?? []).map((s) => `- axis ${s.axis}: ${s.state} (${s.executed}/${s.entries} executed, ${s.crowds} crowd)`),
6501
+ ...(exec.skeleton ?? []).map((s) => `- axis ${s.axis}: ${s.state} (${s.executed}/${s.entries} executed, ${s.crowds} crowd${s.awaiting ? `, ${s.awaiting} awaiting` : ""})`),
6452
6502
  ];
6453
6503
  } catch (e) { rows = [`- (receipt table unavailable — read + audit the receipt file directly: ${P.planExecution})`]; note(`plan-audit receipt table (non-fatal): ${e.message}`); }
6454
6504
  return lines(
@@ -6716,7 +6766,7 @@ export function readFindingsForReport(P) {
6716
6766
  // The matter's FULL in-scope Nice-class set (incl. services 42/44) — declared classes (top-level OR per-mark)
6717
6767
  // else the profile defaults. The dangerous-band floor + its coverage gate must span ALL of these, never a
6718
6768
  // goods-only subset (the VELTRIPHEN services-class miss). Strings, deduped.
6719
- function inScopeClassList(job, profile) {
6769
+ export function inScopeClassList(job, profile) { // @internal — also read by scripts/register-plan-shape.mjs, which must resolve scope exactly as the run did
6720
6770
  const fromMarks = Array.isArray(job?.marks) ? job.marks.flatMap((m) => (Array.isArray(m?.classes) ? m.classes : [])) : [];
6721
6771
  const declared = [...(Array.isArray(job?.classes) ? job.classes : []), ...fromMarks];
6722
6772
  return [...new Set((declared.length ? declared : (profile?.defaultClasses ?? [])).map(String))];
@@ -7035,7 +7085,14 @@ export function coverageRowAreaLabel(axis, unit) { // @internal
7035
7085
  export function coverageJudgmentRows(ledgerRows, planExecution) { // @internal
7036
7086
  const open = [];
7037
7087
  for (const r of ledgerRows ?? []) {
7038
- if (!r || String(r.status ?? "").toLowerCase() === "confirmed-clean") continue;
7088
+ // `withheld-by-judgment` joins `confirmed-clean` in NOT reaching the reader, and for the opposite
7089
+ // reason. A clean row has nothing to disclose. A withheld one has something to say, and it is
7090
+ // ruled to belong in the run record and the coverage ledger only: nothing is added to the report
7091
+ // (owner, 2026-09-18). A family the reading turn chose not to open, having read the identical question as
7092
+ // a list and found what it needed, is not a gap in the client's search — it is where the work was
7093
+ // spent — and a row saying otherwise would read to a lawyer as an incomplete job.
7094
+ const status = String(r?.status ?? "").toLowerCase();
7095
+ if (!r || status === "confirmed-clean" || status === "withheld-by-judgment") continue;
7039
7096
  const reason = String(r.reason ?? "").replace(/\s+/g, " ").trim().slice(0, 160);
7040
7097
  const axis = String(r.axis ?? "").toLowerCase();
7041
7098
  open.push({ axis, area: coverageRowArea(axis, r.unit), areaLabel: coverageRowAreaLabel(axis, r.unit),
@@ -8455,6 +8512,42 @@ function postponeRun(e, run, meta = {}) {
8455
8512
  return { ok: false, postponed: true, resetsAt, codename, fromStage: e.stage, runDir: run?.runDir ?? null };
8456
8513
  }
8457
8514
 
8515
+ /**
8516
+ * The instructed scope a job asks for — what the MATTER named, before any model ran.
8517
+ *
8518
+ * Pulled out of the run so it can be driven straight from a job, because the defect it closes lived
8519
+ * exactly in the seam between the intake gate and this object: the gate counts a job as carrying a
8520
+ * goods description under EITHER spelling, and this stamped only the current one. A job written the
8521
+ * older way passed the gate and landed `goods: null` — the scope file saying the matter named no
8522
+ * goods while the request plainly did — and every reader that asks what the matter covers reads this
8523
+ * file. Nothing said so, because nothing compared the two sites.
8524
+ *
8525
+ * PURE: a job in, a plain object out, no IO.
8526
+ */
8527
+ export function instructedScopeOf(job) {
8528
+ const markNames = Array.isArray(job?.marks)
8529
+ ? job.marks.map((m) => (typeof m === "string" ? m : m?.name)).filter(Boolean)
8530
+ : (job?.markName ?? job?.name ?? null);
8531
+ return {
8532
+ marks: markNames,
8533
+ classes: job?.classes ?? null,
8534
+ jurisdictions: job?.jurisdictions ?? null,
8535
+ // THE GATE'S OWN READING, imported rather than restated. A run's job is folded onto one field at
8536
+ // assembly, so this is normally reading what is already there; it stays for a job handed to this
8537
+ // function directly, and because the gate and the scope disagreeing is the defect it closes.
8538
+ goods: goodsOf(job),
8539
+ customer: job?.customer ?? null,
8540
+ // the geography stamp (enqueue-schema.mjs, "the GEOGRAPHY STAMP": {mode, origin}) — copied
8541
+ // VERBATIM, never recomputed: foldRecipeScope mutates job.jurisdictions on later passes (and
8542
+ // re-stamps origin "saved-search" when it does), so by read time the stamp is the only surviving
8543
+ // record of where the territories came from. Without it here, the frame reconstructs that
8544
+ // provenance from the request prose — a reconstruction validators.matterContext cannot check.
8545
+ // null = the job predates the stamp ("unrecorded", effective-scope.mjs) — an explicit state,
8546
+ // never a missing key.
8547
+ geography: job?.geography ?? null,
8548
+ };
8549
+ }
8550
+
8458
8551
  async function pipelineInner(job, opts = {}) {
8459
8552
  assertTierSanity();
8460
8553
  // The engine binary, first and UNCONDITIONALLY — before the register preflight, which two lanes skip.
@@ -8872,24 +8965,7 @@ async function pipelineInner(job, opts = {}) {
8872
8965
  // frame validator compares against THIS file, never against the frame's own paraphrase) —
8873
8966
  // paraphrase drift between the request and the frame is a defect, not a style choice.
8874
8967
  try {
8875
- const markNames = Array.isArray(job.marks)
8876
- ? job.marks.map((m) => (typeof m === "string" ? m : m?.name)).filter(Boolean)
8877
- : (job.markName ?? job.name ?? null);
8878
- writeFileSync(P.instructedScope, JSON.stringify({
8879
- marks: markNames,
8880
- classes: job.classes ?? null,
8881
- jurisdictions: job.jurisdictions ?? null,
8882
- goods: job.goods ?? null,
8883
- customer: job.customer ?? null,
8884
- // the geography stamp (enqueue-schema.mjs, "the GEOGRAPHY STAMP": {mode, origin}) — copied
8885
- // VERBATIM, never recomputed: foldRecipeScope mutates job.jurisdictions on later passes (and
8886
- // re-stamps origin "saved-search" when it does), so by read time the stamp is the only
8887
- // surviving record of where the territories came from. Without it here, the frame reconstructs
8888
- // that provenance from the request prose — a reconstruction validators.matterContext cannot
8889
- // check. null = the job predates the stamp ("unrecorded", effective-scope.mjs) — an explicit
8890
- // state, never a missing key.
8891
- geography: job.geography ?? null,
8892
- }, null, 2) + "\n");
8968
+ writeFileSync(P.instructedScope, JSON.stringify(instructedScopeOf(job), null, 2) + "\n");
8893
8969
  } catch (e) { note(`instructed-scope write failed (non-fatal): ${e.message}`); }
8894
8970
  // THE STORED DEFAULTS THE ENGINE CANNOT SEARCH — recorded by the run, not only by the plan preview.
8895
8971
  //
@@ -9820,6 +9896,19 @@ async function pipelineInner(job, opts = {}) {
9820
9896
  const fanInRepairs = [];
9821
9897
  const dispatchPlanQids = async (a, qids, repairId, max = 1) => {
9822
9898
  if (!planExec || !ctx.registerPlan) return null;
9899
+ // A qid this run has accepted as a capability gap is never sent to the provider again, under any plan
9900
+ // version (envelope-settle.mjs stickyGapsAfter). A whole-axis dispatch becomes the axis's other qids;
9901
+ // the executor's qid-ownership merge keeps the gap's own block as it stands.
9902
+ const sticky = readStickyGaps(P);
9903
+ if (sticky.size) {
9904
+ const wanted = qids?.length ? qids : (ctx.registerPlan.entries ?? []).filter((e) => e.axis === a).map((e) => e.qid);
9905
+ const held = wanted.filter((q) => sticky.has(q));
9906
+ if (held.length) {
9907
+ runLog(run.runDir, { event: "plan-qids-sticky-gap", axis: a, qids: held, action: repairId });
9908
+ qids = wanted.filter((q) => !sticky.has(q));
9909
+ if (!qids.length) return null;
9910
+ }
9911
+ }
9823
9912
  if (!repairLedger.canAttempt(repairId, a, { max, epoch: repairEpoch })) return null;
9824
9913
  note(`register-unit ${a}: ${repairId} — direct executor dispatch (code, no agent turn) for ${qids?.length ?? "all"} dictated slice(s)`);
9825
9914
  let outcome;
@@ -10280,7 +10369,15 @@ async function pipelineInner(job, opts = {}) {
10280
10369
  // been a log line. Settling here is idempotent and makes the decision durable.
10281
10370
  if (!receiptSettled(P, priorReceipt).settled) await settleEnvelopeAtReceipt("receipt-reuse");
10282
10371
  } else {
10283
- let joinRes = joinPlanToBands(ctx.registerPlan, readBands());
10372
+ // A qid this run has accepted as a capability gap does not ride the ladder again. It leaves `missing`
10373
+ // for `deferred` with the reason it was accepted under, before any dispatch or followup reads the
10374
+ // join, so neither the direct executor nor the plan-join followup re-dictates it.
10375
+ const holdStickyGaps = (j) => {
10376
+ const held = holdStickyGapsIn(j, readStickyGaps(P));
10377
+ if (held !== j) runLog(run.runDir, { event: "plan-qids-sticky-gap", qids: j.missing.filter((q) => !held.missing.includes(q)), action: "held-from-missing" });
10378
+ return held;
10379
+ };
10380
+ let joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
10284
10381
  const missingEntriesByAxis = () => {
10285
10382
  const byQid = new Map(ctx.registerPlan.entries.map((e) => [e.qid, e]));
10286
10383
  const m = new Map();
@@ -10302,7 +10399,7 @@ async function pipelineInner(job, opts = {}) {
10302
10399
  runLog(run.runDir, { event: "plan-qids-missing", axis: a, qids, action: "plan-direct-execute" });
10303
10400
  await dispatchPlanQids(a, qids, "plan-direct-execute", 2);
10304
10401
  }
10305
- joinRes = joinPlanToBands(ctx.registerPlan, readBands());
10402
+ joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
10306
10403
  if (!joinRes.missing.length) deriveNamedBand(ctx); // dispatch landed blocks — re-merge so Layer B reads them
10307
10404
  }
10308
10405
  if (joinRes.missing.length) {
@@ -10327,7 +10424,7 @@ async function pipelineInner(job, opts = {}) {
10327
10424
  const wf = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "plan-join" });
10328
10425
  if (!wf.ok) note(`register-unit ${a}: warm plan-join followup failed (${wf.fail}) — the plan-unexecuted StageFailure below holds the line`);
10329
10426
  }
10330
- joinRes = joinPlanToBands(ctx.registerPlan, readBands());
10427
+ joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
10331
10428
  deriveNamedBand(ctx); // the followup appended band blocks — re-merge so Layer B reads them
10332
10429
  }
10333
10430
  const skeleton = writeExecution(joinRes);
@@ -10402,6 +10499,7 @@ async function pipelineInner(job, opts = {}) {
10402
10499
  { failClass, repairs: fanInRepairs, quantity: joinRes.missing.length });
10403
10500
  }
10404
10501
  runLog(run.runDir, { event: "plan-execution", executed: joinRes.executed.length, skipped: joinRes.skipped.length, unplanned: joinRes.unplanned.length,
10502
+ awaiting: joinRes.awaiting?.length ?? 0, asked: joinRes.asked?.length ?? 0,
10405
10503
  axes: skeleton.map((s) => `${s.axis}:${s.state}`) });
10406
10504
  // Decide the deferrals now — before placement-inquiry, which on the evidence run started one second
10407
10505
  // after this point on inputs the run had just recorded as unfinished.
@@ -11610,7 +11708,7 @@ async function pipelineInner(job, opts = {}) {
11610
11708
  // that designates no floor.
11611
11709
  if (owned.length > 0 && !floorBreachAxes.has(a.toLowerCase())) {
11612
11710
  const split = splitDeferredByCloseability(ledger, a, escalationGapAxes,
11613
- { fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()) });
11711
+ { fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()), heldUnits: stickyGapUnits(P) });
11614
11712
  const openNonDeferred = owned.some((r) => r.status !== "deferred" && r.status !== "coverage-limited");
11615
11713
  if (split.held.length > 0 && split.closeable.length === 0 && !openNonDeferred) {
11616
11714
  note(`escalation skipped ${a} — capability-gap deferral: the active register provider cannot express those slices, so a re-run re-derives the same refusal (the gap stays open and disclosed)`);
@@ -11686,7 +11784,7 @@ async function pipelineInner(job, opts = {}) {
11686
11784
  // while ALSO carrying held rows — and the resumed unit must not be left to think those are
11687
11785
  // work it failed to do. Name them, exactly as the envelope's close followup does.
11688
11786
  const escHeld = splitDeferredByCloseability(ledger, a, escalationGapAxes,
11689
- { fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()) }).held;
11787
+ { fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()), heldUnits: stickyGapUnits(P) }).held;
11690
11788
  if (escHeld.length) {
11691
11789
  followup += `\n\nNOT YOURS TO CLOSE — the active register provider cannot express these slices at all, so no re-run can reach them. Leave these Coverage-ledger rows exactly as they are (\`deferred\`, same reason), and do not restate them as searched or clean:\n${escHeld.map((r) => `| ${r.unit} | deferred | ${r.reason} |`).join("\n")}`;
11692
11790
  }
@@ -11762,8 +11860,9 @@ async function pipelineInner(job, opts = {}) {
11762
11860
  // time, still deferred, still an open floor, still disclosed.
11763
11861
  const gapAxes = capabilityGapAxes(ctx.registerPlan, readPlanExecution(ctx));
11764
11862
  const fullyDeferred = new Set((ctx.registerPlan ? fullyDeferredAxes(ctx.registerPlan) : []).map((a) => String(a.axis).toLowerCase()));
11863
+ const stickyUnitsNow = stickyGapUnits(P); // a gap this run already accepted is held whatever its row's reason says
11765
11864
  const closeabilityByAxis = new Map(deferredAxes.map((a) => [a,
11766
- splitDeferredByCloseability(ledgerNow, a, gapAxes, { fullyDeferred: fullyDeferred.has(String(a).toLowerCase()) })]));
11865
+ splitDeferredByCloseability(ledgerNow, a, gapAxes, { fullyDeferred: fullyDeferred.has(String(a).toLowerCase()), heldUnits: stickyUnitsNow })]));
11767
11866
  const heldAxes = deferredAxes.filter((a) => {
11768
11867
  const s = closeabilityByAxis.get(a);
11769
11868
  // The `!hasBreach &&` term is retired with the ⭐ floor: it kept an axis carrying a
@@ -11805,6 +11904,10 @@ async function pipelineInner(job, opts = {}) {
11805
11904
  ? `\n\nThese rows on this axis are NOT yours to close and must stay exactly as they are — the active register provider cannot express those slices at all, so no re-run can reach them. Leave them \`deferred\`, keep their reason, and do not restate them as searched or clean:\n${split.held.map((r) => `| ${r.unit} | deferred | ${r.reason} |`).join("\n")}`
11806
11905
  : "";
11807
11906
  note(`envelope: closing deferred coverage on ${a} (deadline permits — ${decision.reason})`);
11907
+ // What the re-opened unit is asked to close, and what it is told to leave: the record of the split
11908
+ // this close acted on, so a gap handed back as work is visible in the run's own log.
11909
+ runLog(run.runDir, { event: "envelope-close-rows", axis: a,
11910
+ closeable: split.closeable.map((r) => String(r.unit ?? "")), held: split.held.map((r) => String(r.unit ?? "")) });
11808
11911
  const followup = repairFollowup("register-unit:envelope-close", { paths: P, axis: a, rows: rows + heldRows,
11809
11912
  supplementalLane: !!ctx.registerPlan?.contract?.supplemental_lane });
11810
11913
  const r = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "envelope" });
@@ -12241,6 +12344,7 @@ async function pipelineInner(job, opts = {}) {
12241
12344
  ledger: loadCoverageLedger(run.runDir).rows, // fresh read — post-reopen/re-digest, same source as every gate
12242
12345
  planContext: { entries: ctx.registerPlan.entries ?? [], niceClasses: ctx.registerPlan.nice_classes ?? [], regions: ctx.registerPlan.regions ?? [] },
12243
12346
  executor: ccExecutor,
12347
+ capabilities: registerCapabilities(), // the contains floor, read the way the plan compile reads it
12244
12348
  note: (m) => note(m),
12245
12349
  log: (row) => runLog(run.runDir, row), // the orchestrator's own crowd-context-failed row lands in run.jsonl
12246
12350
  });
@@ -12499,7 +12603,7 @@ async function pipelineInner(job, opts = {}) {
12499
12603
  : "failed after retries + fallback (the ladder recorded no condition)");
12500
12604
  let verdict = firstRef.verdict;
12501
12605
  runLog(run.runDir, { event: "verdict", verdict });
12502
- writeRunStatus(ctx, { verdict });
12606
+ writeRunStatus(ctx, signoffPatch(verdict));
12503
12607
  note(`refutation verdict: ${verdict}`);
12504
12608
  // qw/typed-correction-kinds — TELEMETRY ONLY (data first; the corrective-skip decision is a
12505
12609
  // separate owner-gated build): partition the review's flagged-correction lines by their
@@ -12743,7 +12847,7 @@ async function pipelineInner(job, opts = {}) {
12743
12847
  verdict = rc.ok ? (parseVerdict(readFileSync(P.seniorEyeReview, "utf8")) ?? entryVerdict) : entryVerdict;
12744
12848
  correctiveRecheckOk = rc.ok === true;
12745
12849
  runLog(run.runDir, { event: "verdict-2", verdict });
12746
- writeRunStatus(ctx, { verdict });
12850
+ writeRunStatus(ctx, signoffPatch(verdict));
12747
12851
  }
12748
12852
  // AD-2 A1 — the completion receipt, written ONLY when the cycle finished with a verified recheck:
12749
12853
  // a fallen-back recheck kept the entry verdict WITHOUT re-verifying, and cementing that would stop
@@ -12985,7 +13089,7 @@ async function pipelineInner(job, opts = {}) {
12985
13089
  if (d.clamped) {
12986
13090
  runLog(run.runDir, { event: "terminal-guard-clamp", defect: d.record.defect, from: verdict, to: d.verdict });
12987
13091
  verdict = d.verdict;
12988
- writeRunStatus(ctx, { verdict });
13092
+ writeRunStatus(ctx, signoffPatch(verdict));
12989
13093
  }
12990
13094
  };
12991
13095
 
@@ -13119,7 +13223,7 @@ async function pipelineInner(job, opts = {}) {
13119
13223
  runLog(run.runDir, { event: "coverage-floor-clamp", cause: "forward-actions", from: "CLEAR", to: "CONDITIONAL", legalActions: conditions.length });
13120
13224
  note(`deliver-conditional floor: the opinion names ${conditions.length} forward legal action(s) a human must take (${conditions[0].slice(0, 140)}${conditions.length > 1 ? `; +${conditions.length - 1} more` : ""}) — clamping CLEAR→CONDITIONAL (spec 64: the disposition is derived from the findings' named actions).`);
13121
13225
  verdict = "CONDITIONAL";
13122
- writeRunStatus(ctx, { verdict });
13226
+ writeRunStatus(ctx, signoffPatch(verdict));
13123
13227
  } else if (fresh.length) {
13124
13228
  runLog(run.runDir, { event: "verdict-conditions-recorded", legalActions: fresh.length });
13125
13229
  }
@@ -13157,7 +13261,7 @@ async function pipelineInner(job, opts = {}) {
13157
13261
  if (droppedConditions.length && verdict === "CLEAR") {
13158
13262
  runLog(run.runDir, { event: "coverage-floor-clamp", cause: "dropped-actions", from: "CLEAR", to: "CONDITIONAL", actionsDropped: droppedConditions.length });
13159
13263
  verdict = "CONDITIONAL";
13160
- writeRunStatus(ctx, { verdict });
13264
+ writeRunStatus(ctx, signoffPatch(verdict));
13161
13265
  }
13162
13266
  }
13163
13267
  } catch (e) { note(`legal-actions floor skipped (${String(e.message).slice(0, 80)}) — never-kill; the predelivery coherence lint is the second net`); }
@@ -13205,7 +13309,7 @@ async function pipelineInner(job, opts = {}) {
13205
13309
  // re-ask can flip a clamped verdict back to CLEAR, and it must meet this floor again with the
13206
13310
  // reason ALREADY recorded. Folding these two into one guard would let the second pass skip on
13207
13311
  // the dedup and deliver CLEAR — the precise defect this issue is about.
13208
- if (clReason && verdict === "CLEAR") { verdict = "CONDITIONAL"; writeRunStatus(ctx, { verdict }); }
13312
+ if (clReason && verdict === "CLEAR") { verdict = "CONDITIONAL"; writeRunStatus(ctx, signoffPatch(verdict)); }
13209
13313
  }
13210
13314
  } catch (e) { note(`common-law downgrade floor skipped (${String(e.message).slice(0, 80)}) — never-kill`); }
13211
13315
  if (entryVerdict === "CLEAR") {
@@ -13299,7 +13403,7 @@ async function pipelineInner(job, opts = {}) {
13299
13403
  // The reason KINDS distinguish coverage/frame/screen-gate/senior-right/register residue for the
13300
13404
  // report bound line and the client conditions row (merged — legalActions survives).
13301
13405
  clampKinds = { ...clampKinds, coverage: coverageInsufficient || undefined, frame: frameResidual || undefined, screenGate: screenGateGap || undefined, seniorRight: seniorGap || undefined, registerGap: registerGap || undefined, deadlineCarry: deadlineGap || undefined };
13302
- writeRunStatus(ctx, { verdict });
13406
+ writeRunStatus(ctx, signoffPatch(verdict));
13303
13407
  }
13304
13408
  }
13305
13409
  };
@@ -13487,7 +13591,7 @@ async function pipelineInner(job, opts = {}) {
13487
13591
  if (rr.ok) {
13488
13592
  verdict = parseVerdict(readReview()) ?? "BLOCKING";
13489
13593
  runLog(run.runDir, { event: "verdict-3", verdict, trigger: "degenerate-reask" });
13490
- writeRunStatus(ctx, { verdict });
13594
+ writeRunStatus(ctx, signoffPatch(verdict));
13491
13595
  applyCoverageFloor(); // a re-asked CLEAR passes the same deliver-conditional floor
13492
13596
  try { writeVerdictSidecar(); }
13493
13597
  catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
@@ -14512,7 +14616,7 @@ async function pipelineInner(job, opts = {}) {
14512
14616
  applyCoverageFloor();
14513
14617
  try { writeVerdictSidecar(); }
14514
14618
  catch (e) { throw new StageFailure("verdict", `verdict sidecar re-derive failed (spec-64 coherence repair): ${String(e.message).slice(0, 120)}`); }
14515
- if (verdict !== beforeVerdict) writeRunStatus(ctx, { verdict });
14619
+ if (verdict !== beforeVerdict) writeRunStatus(ctx, signoffPatch(verdict));
14516
14620
  runLog(run.runDir, { event: "verdict-rederive-repair", from: beforeVerdict, to: verdict });
14517
14621
  lint = lintNow();
14518
14622
  }
@@ -14798,7 +14902,7 @@ async function pipelineInner(job, opts = {}) {
14798
14902
  runLog(run.runDir, { event: "verdict-hardened-by-repair", was: verdict, now: hardened, stage: s2.label, adopted: true });
14799
14903
  note(`stale-repair: the reviewer returned ${hardened} — adopting it as the run's verdict (the one settled earlier was decided against a review this repair has since rewritten) and rebuilding the label from it`);
14800
14904
  verdict = hardened;
14801
- writeRunStatus(ctx, { verdict });
14905
+ writeRunStatus(ctx, signoffPatch(verdict));
14802
14906
  try { writeVerdictSidecar(); }
14803
14907
  catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
14804
14908
  reassemble = true;
@@ -15295,7 +15399,7 @@ async function pipelineInner(job, opts = {}) {
15295
15399
  {
15296
15400
  const mark = job.markName ?? job.name ?? job.ref ?? "the matter";
15297
15401
  const ref = job.ref ? ` (${job.ref})` : "";
15298
- const vtag = verdict ? ` — verdict ${verdict}` : "";
15402
+ const vtag = emailVerdictOpts.tier ? ` Overall risk: ${emailVerdictOpts.tier}.` : ""; // the rating, never the reviewer's sign-off word (ruled 2026-09-22)
15299
15403
  // self-contained packet (the email body HTML is embedded so the courier needs no path resolution
15300
15404
  // across the archive move). The courier sends EXACTLY this — same subject, text and recipient the
15301
15405
  // deleted send stages composed, now composed in code.
@@ -15317,7 +15421,7 @@ async function pipelineInner(job, opts = {}) {
15317
15421
  // stated reason when no number is held. It used to go to AGENT_WHATSAPP[agent], which is the
15318
15422
  // operator on every run because every user shares one agent id.
15319
15423
  ...whatsappRouting(job, agent),
15320
- whatsappText: `✅ Clearotron search for ${mark}${ref}${vtag} is done. Report: ${published.url}`,
15424
+ whatsappText: `✅ Clearotron search for ${mark}${ref} is done.${vtag} Report: ${published.url}`,
15321
15425
  url: published.url, verdict, markName: job.markName ?? job.name ?? null,
15322
15426
  };
15323
15427
  // A NEW send supersedes any previous one: .sent is PER-SEND idempotence, not per-run-lifetime.
@@ -15364,13 +15468,13 @@ async function pipelineInner(job, opts = {}) {
15364
15468
  // .published + .delivered on disk must never read "7/9" on any status surface (nothing runs after
15365
15469
  // the packet, so no stage transition would ever finish the display sequence). finalStepFields ⇒ 9/9.
15366
15470
  const deliveredAt = new Date().toISOString();
15367
- writeRunStatus(ctx, { state: "delivered", verdict, statement: emailVerdictOpts.statement ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
15471
+ writeRunStatus(ctx, { state: "delivered", ...signoffPatch(verdict), statement: emailVerdictOpts.statement ?? undefined, caption: published.caption ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
15368
15472
  // — THE POOL COPY LEARNS ITS OWN TERMINAL STATE, HERE AND NOWHERE ELSE.
15369
15473
  // `meta.json` cannot carry this: it is composed inside publish, before this line runs, so the state
15370
15474
  // did not exist yet when it was written. This is the one moment where the terminal state and the
15371
15475
  // pool directory are both in hand. Best-effort by construction — the report is already published
15372
15476
  // and the run is already settled, so a failure to stamp is logged and the delivery proceeds.
15373
- const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", verdict, deliveredAt, runId: published.runId ?? run.runId, lane: "clearance" });
15477
+ const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", signoff: verdict, deliveredAt, runId: published.runId ?? run.runId, lane: "clearance" });
15374
15478
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
15375
15479
  const archived = archive(run);
15376
15480
  rollupStatus(run.studioRoot);
@@ -15939,7 +16043,7 @@ export function reconstructCtx(job, opts) { // @internal
15939
16043
  } catch { /* no/corrupt frozen plan — prose axes stand, as they always did */ }
15940
16044
  const readJson = (f) => { try { return JSON.parse(readFileSync(join(run.runDir, f), "utf8")); } catch { return null; } };
15941
16045
  const status = readJson("status.json");
15942
- if (status?.verdict) ctx.verdict = status.verdict;
16046
+ if (readSignoff(status)) ctx.verdict = readSignoff(status); // `review.signoff`, or `verdict` on a record written before the move
15943
16047
  const pub = readJson(".published");
15944
16048
  if (pub?.url) ctx.publishedUrl = pub.url;
15945
16049
  // ── THE CTX FIELDS THIS USED TO DROP (item) ───────────────────────────────────────────────────
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sarl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // DO describe_options AND plan_run AGREE ON WHICH PRODUCTS ARE AVAILABLE — the deployment check's arm,
5
+ // extracted so it can be driven without a door to call (the reason surface-exit-verdict.mjs gives).
6
+ //
7
+ // A plan_run that THROWS compared nothing. A 429 from the door's rate limit, a timeout, a refused
8
+ // connection: each means the question was never answered, and the check used to file it beside the real
9
+ // disagreements and exit 1, "drifted". Three runs of the check in two minutes exhausted the ops door's
10
+ // rate limit and the third reported a drift that a redeploy could not have fixed. So an unanswered ask is
11
+ // a marked skip, which exits 3, and only an answer that contradicts describe_options is a drift.
12
+
13
+ // The blocker wording plan_run uses for a product it will not run.
14
+ const UNAVAILABLE = /not part of the current release|not switched on|unavailable/i;
15
+
16
+ /**
17
+ * Ask plan_run about each product describe_options listed, and say whether the two agree.
18
+ *
19
+ * @param {{ keys: string[], doorSays: Map<string, boolean>, probeProfileKey: string|null,
20
+ * ask: (key: string) => Promise<{blockers?: unknown[]}> }} input
21
+ * `ask` is the plan_run call for one product; a throw is a question that was not answered.
22
+ * @returns {Promise<{state: "pass"|"fail"|"skip", message: string, blocked?: true}>}
23
+ */
24
+ export async function planRunAgreementVerdict({ keys = [], doorSays = new Map(), probeProfileKey = null, ask }) {
25
+ if (!probeProfileKey) {
26
+ return { state: "skip", message: "no customer resolved to plan against — see the roster check" };
27
+ }
28
+ const disagreements = [];
29
+ const unanswered = [];
30
+ for (const key of keys) {
31
+ let plan;
32
+ try { plan = await ask(key); } catch (e) { unanswered.push(`${key}: ${String(e?.message ?? e).slice(0, 120)}`); continue; }
33
+ const unavailable = (plan?.blockers ?? []).some((b) => UNAVAILABLE.test(String(b)));
34
+ if (unavailable !== !doorSays.get(key)) {
35
+ disagreements.push(`${key}: describe_options=${doorSays.get(key) ? "available" : "unavailable"} plan_run=${unavailable ? "unavailable" : "available"}`);
36
+ }
37
+ }
38
+ // A drift outranks an unanswered ask, as in exitFor: the drift is actionable now. The unanswered
39
+ // products stay on the line so the report does not read as though they were compared.
40
+ if (disagreements.length) {
41
+ return { state: "fail", message: disagreements.join(" · ")
42
+ + (unanswered.length ? ` · and plan_run gave no answer for ${unanswered.length}: ${unanswered.join(" · ")}` : "") };
43
+ }
44
+ if (unanswered.length) {
45
+ return { state: "skip", blocked: true, message: `could not ask plan_run about ${unanswered.length} of ${keys.length} product(s), `
46
+ + `so those were not compared — ${unanswered.join(" · ")}` };
47
+ }
48
+ return { state: "pass", message: `${keys.length} products checked through both code paths` };
49
+ }
@@ -350,7 +350,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
350
350
  const docs = reportsOf(meta).filter((r) => existsSync(join(poolRoot, name, r.file)));
351
351
  const hasReport = docs.length > 0;
352
352
  const { bands, toneFor } = ladderOf(meta);
353
- const band = meta.overall ?? meta.verdict ?? null;
353
+ const band = meta.overall ?? null; // the rating; a meta's retired `verdict` held the reviewer's sign-off on the full-search lane
354
354
  out.push({ runId: meta.runId ?? name, account: owner, ...(owner === "generic" ? { organisation: meta.organisation ?? null } : {}),
355
355
  title: meta.title ?? meta.matter ?? name, kind: meta.kind ?? "clearance",
356
356
  // THE MARK, separate from the report's headline.
@@ -540,7 +540,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
540
540
  // hide this. Staff only: the client redaction below replaces `reason` and must drop this too,
541
541
  // or the redaction would be defeated by the field that carries the raw words.
542
542
  reasonDetail: s.reasonDetail ?? null,
543
- date: (s.updatedAt ?? "").slice(0, 10), overall: s.verdict ?? null,
543
+ date: (s.updatedAt ?? "").slice(0, 10), overall: s.tier ?? null, // the rating, never the reviewer's sign-off word
544
544
  // A live run has not been issued, so its last progress write is the honest ordering key — the
545
545
  // same role issuedAt plays for a delivered one, never presented as a finish time.
546
546
  issuedAt: typeof s.updatedAt === "string" ? s.updatedAt : null,
@@ -4006,7 +4006,7 @@ ${signedIn
4006
4006
  <div><a href="/portal">Go to the portal</a></div>
4007
4007
  <form method="post" action="/portal/logout"><button type="submit">Sign out</button></form>`
4008
4008
  : `<p>This ${escHtml(BRAND.name)} has one user: <b class="who">${escHtml(email)}</b>. Enter its passphrase.</p>
4009
- ${discarded ? `<p class="hint">A session this portal did not start, from another ${escHtml(BRAND.name)} on this address or an expired one, was set aside. Sign in below.</p>` : ""}
4009
+ ${discarded ? `<p class="hint">Your earlier sign-in has expired. Sign in again.</p>` : ""}
4010
4010
  ${error ? `<p class="err">${escHtml(error)}</p>` : ""}
4011
4011
  <form method="post" action="/portal/login">
4012
4012
  <label for="passphrase">Passphrase</label>
@@ -4130,6 +4130,10 @@ async function readFormBody(req, limitBytes = 8192) {
4130
4130
  });
4131
4131
  }
4132
4132
 
4133
+ // The brief reader's model when PORTAL_READ_MODEL is unset: a TIER, like every stage's. An exact id goes to
4134
+ // the program as that model — past a cloud's ANTHROPIC_DEFAULT_SONNET_MODEL — and codex maps tiers only.
4135
+ export const PORTAL_READ_MODEL_DEFAULT = "sonnet";
4136
+
4133
4137
  export function makeHttpHandler({ verify, limiter, service, log = () => {}, devIdentity = null, localAuth = null, static: staticHandler = null,
4134
4138
  // item 1 — PASSED IN, because this handler is its own function and the bootstrap that reads
4135
4139
  // the environment is another. The default is the Cloudflare Access header, so a caller that does
@@ -5211,7 +5215,7 @@ const PORT = PORT_CHOICE.port;
5211
5215
  // two are parameters on the shared runner for exactly this reason: `compose-read.mjs` records that
5212
5216
  // Haiku 4.5 refuses a thinking block outright (400), so inheriting the jx constants here would
5213
5217
  // have been a failure on every press.
5214
- const model = process.env.PORTAL_READ_MODEL || "claude-sonnet-5";
5218
+ const model = process.env.PORTAL_READ_MODEL || PORTAL_READ_MODEL_DEFAULT;
5215
5219
  const runner = await makeJxTurnRunner({ model, thinking: "off", lane: "the brief reader" });
5216
5220
  if (runner?.error) {
5217
5221
  // NAMES THE CONDITION, on the operator's surface. The client-facing note stays client-facing;