clearotron 0.5.0-beta.2 → 0.5.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.env.example +7 -0
  2. package/build-info.json +2 -2
  3. package/docs/architecture/03-run-lifecycle.md +6 -0
  4. package/docs/architecture/04-configuration-reference.md +1 -0
  5. package/docs/architecture/05-config-governance.md +1 -1
  6. package/driver/CHANGELOG.md +21 -0
  7. package/driver/band-shape.mjs +19 -11
  8. package/driver/contract-vocabulary.mjs +3 -3
  9. package/driver/coverage-form-io.mjs +3 -1
  10. package/driver/coverage-form.mjs +21 -2
  11. package/driver/coverage-ledger.mjs +7 -1
  12. package/driver/declination-call.mjs +33 -16
  13. package/driver/degraded-parts.mjs +6 -0
  14. package/driver/engine/mcp/band-server.mjs +12 -12
  15. package/driver/engine/mcp/clarivate-server.mjs +2 -0
  16. package/driver/engine/mcp/codex-config.mjs +12 -7
  17. package/driver/engine/mcp/corsearch-server.mjs +2 -0
  18. package/driver/engine/mcp/declination-server.mjs +4 -5
  19. package/driver/engine/mcp/dispositions-server.mjs +7 -0
  20. package/driver/engine/mcp/euipo-server.mjs +2 -0
  21. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  22. package/driver/engine/mcp/gather-config.mjs +15 -0
  23. package/driver/engine/mcp/held-answers.mjs +11 -3
  24. package/driver/engine/mcp/perplexity-server.mjs +29 -1
  25. package/driver/engine/mcp/recording-server.mjs +16 -1
  26. package/driver/engine/mcp/signa-server.mjs +2 -0
  27. package/driver/engine/mcp/supplemental.mjs +11 -3
  28. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  29. package/driver/engine/openai-agent.mjs +48 -9
  30. package/driver/envelope-settle.mjs +13 -3
  31. package/driver/findings-model.mjs +17 -0
  32. package/driver/gateway.mjs +31 -10
  33. package/driver/package.json +1 -1
  34. package/driver/parts-not-completed.mjs +52 -11
  35. package/driver/pipeline-knockout.mjs +5 -4
  36. package/driver/pipeline.mjs +474 -81
  37. package/driver/placement-decisions.mjs +24 -9
  38. package/driver/placement-form-io.mjs +21 -1
  39. package/driver/placement-form.mjs +25 -15
  40. package/driver/placement-union.mjs +53 -8
  41. package/driver/predelivery-lint.mjs +8 -1
  42. package/driver/publish/index.mjs +7 -3
  43. package/driver/register-digest-record.mjs +6 -3
  44. package/driver/research-answers.mjs +54 -7
  45. package/driver/runner.mjs +4 -4
  46. package/driver/saved-findings-block.mjs +36 -0
  47. package/driver/skills/clearance-register/digest.md +1 -1
  48. package/driver/skills/clearance-search/phase2-execution.md +7 -5
  49. package/driver/skills/clearance-search/synthesis-rules.md +3 -3
  50. package/driver/skills/placement-inquiry/SKILL.md +3 -3
  51. package/driver/stage-context.mjs +18 -0
  52. package/driver/stages.mjs +41 -13
  53. package/driver/suite-census.json +140 -14
  54. package/driver/synthesis-record.mjs +167 -1
  55. package/driver/verify.mjs +6 -6
  56. package/mcp-server/CHANGELOG.md +8 -0
  57. package/mcp-server/package.json +1 -1
  58. package/package.json +1 -1
  59. package/portal-ui/package.json +1 -1
  60. package/providers/_shared/README.md +1 -0
  61. package/providers/_shared/enumerate.mjs +4 -3
  62. package/providers/_shared/execute-plan.mjs +33 -7
  63. package/providers/_shared/send-until.mjs +25 -0
  64. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  65. package/providers/oauth-mcp-bridge/package.json +1 -1
  66. package/scripts/band-shape-probe.mjs +1 -1
  67. package/scripts/e2e-first-time.mjs +2 -1
  68. package/scripts/release-notes-lint.mjs +1 -1
  69. package/shared/inside-run.mjs +54 -0
  70. package/shared/names-in-force.mjs +1 -0
  71. package/shared/slices-taken-out.mjs +65 -0
  72. package/skills/clearotron-account/SKILL.md +1 -1
  73. package/skills/clearotron-ops/SKILL.md +1 -1
package/.env.example CHANGED
@@ -184,6 +184,13 @@ CLEAROTRON_DATABASE=clarivate
184
184
  # CLEAROTRON_GATHER_SESSION_KEY, CLEAROTRON_GATHER_SESSION_ID, CLEAROTRON_GATHER_AGENT,
185
185
  # CLEAROTRON_REGISTER_CALL_LOG, CLEAROTRON_REGISTER_RECORD_LOG
186
186
  #
187
+ # The time, in epoch milliseconds, at which a turn's register tools stop sending new questions, so the
188
+ # unit can write what it holds inside its own time. The driver writes it per turn into each register
189
+ # tool server's env, a quarter of the turn (at most five minutes) ahead of the turn's deadline. Leave it
190
+ # empty: a value set here would be read by the driver's own code-side executor as a fixed time.
191
+ # effect: tuning
192
+ CLEAROTRON_GATHER_SEND_UNTIL_MS=
193
+ #
187
194
  # THERE IS NO RETENTION SETTING AND NO CLEANUP JOB. CLEAROTRON_REGISTER_LEDGER_MAX_BYTES,
188
195
  # CLEAROTRON_REGISTER_LEDGER_KEEP and `npm run prune:ledger` are gone with the rotation tool, because the
189
196
  # thing they bounded no longer grows: RECORD BODIES ARE WRITTEN INTO THE RUN'S OWN DIRECTORY
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "bdb15f0c3974c46ccb0db2a2020779e65f0b4e21",
3
- "version": "0.5.0-beta.2"
2
+ "commit": "7c6882de2e3c147992a6667bbce24de9fffb5206",
3
+ "version": "0.5.0-beta.4"
4
4
  }
@@ -438,6 +438,7 @@ node pipeline.mjs --job <file.json> [--agent <id>] # cold run (still take
438
438
  node pipeline.mjs --resume <codename> # idempotent resume: valid outputs skip
439
439
  node pipeline.mjs --resume <codename> --from <stage> # force re-run from a stage ordinal
440
440
  node pipeline.mjs --resume <codename> --experiment <stage> [--label <t>] # shadow dir
441
+ node pipeline.mjs --resume <codename> --experiment synthesis --research replay # research from the run's own answers
441
442
  ```
442
443
 
443
444
  - **Resume** refuses a delivered run (any of the three delivered markers above — re-handing off a
@@ -448,6 +449,11 @@ node pipeline.mjs --resume <codename> --experiment <stage> [--label <t>]
448
449
  - **`--experiment`** runs one stage in a shadow dir (`_experiments/<ts>-<tag>/`) on copies of its
449
450
  inputs, under a `clearance-exp-…` session key that is excluded from the run's provider-usage
450
451
  attribution. The canonical run is untouched.
452
+ - **`--research replay`** answers the arm's research from the answers the run saved
453
+ (`_driver/research-answers.jsonl`, copied into the shadow dir). A question the run asked gets the
454
+ run's latest answer to it; a question it did not ask, and any grid, is refused, and nothing is sent
455
+ to the research service. It is refused for a stage that holds no research tool, or a run that saved
456
+ no answers. The arm's `experiment-context.json` records `research: {mode, answers}`.
451
457
  - **Orphan self-resume**: a manually resumed run that parks has no queue sidecars; the runner scans
452
458
  run dirs for due, payload-complete `.postponed` sentinels not owned by any queue and resumes them
453
459
  (claim = atomic rename to `.resuming`).
@@ -373,6 +373,7 @@ cannot be read as one list.
373
373
  | `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
374
374
  | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
375
375
  | `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
376
+ | `CLEAROTRON_GATHER_SEND_UNTIL_MS` | set per turn | When a turn's register tools stop sending new questions: a quarter of the turn, at most five minutes, ahead of its deadline (set by the gather config; not operator-set). |
376
377
  | `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
377
378
  | `PORTAL_READ_MODEL` | `sonnet` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
378
379
  | `CLEAROTRON_ORDER_PROBE_SEED` | unset | Seed for `scripts/band-shape-probe.mjs`, so an ordering probe can be replayed. A diagnostic script's knob, not a run's. |
@@ -282,7 +282,7 @@ edited — that box runs pre-rebuild code and genuinely still sets it.
282
282
  `CLEAROTRON_OUTBOX_BACKOFF_MAX_RETRIES` (5) / `CLEAROTRON_OUTBOX_GIVEUP_COOLDOWN_SEC` (3600 — note this one
283
283
  drops BACKOFF from the prefix, which the elided spelling this row used to carry actively misled about),
284
284
  `CLEAROTRON_OUTBOX_NOPROGRESS_MAX`, `TRADEMARK_MSGID_DOMAIN` (enqueue.local).
285
- Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`.
285
+ Code-set per stage (not operator-set): `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_SESSION_ID`; per turn: `CLEAROTRON_GATHER_SEND_UNTIL_MS`.
286
286
 
287
287
  **Who the completion notice reaches** — `CLEAROTRON_REQUESTER_WHATSAPP` (empty; effect: deployment) is a
288
288
  JSON map of requester email or handle → number, and `CLEAROTRON_WHATSAPP_OPERATOR_COPY` (on unless `0`,
@@ -1,5 +1,26 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.5.0-beta.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: On the Codex engine, a clearance no longer sees temporary files that another clearance left on the same machine.
8
+ - Fixed: A report's copy for the client is no longer held back when a live finding shares its mark and owner with a finding the review withdrew.
9
+ - Fixed: A register entry the engine decided to keep as background now appears in the report's context table, instead of being left out of it.
10
+ - Fixed: When the report's writing step is interrupted, the next attempt continues from the findings it had already finished instead of starting again.
11
+ - Fixed: On a crowded field, the report is written from the work the clearance has already done, without reading every register result again.
12
+
13
+ ## 0.5.0-beta.3
14
+
15
+ ### Patch Changes
16
+
17
+ - Fixed: The delivery email for a result the review could not sign off now lists the report's "Only you can close these" items.
18
+ - Fixed: When a clearance fails, its chat notice now goes to the person who asked for it, with a copy to the operator.
19
+ - Fixed: A clearance no longer repeats a finished step because a web lookup inside it timed out.
20
+ - Fixed: A clearance whose register search cannot finish, such as one that keeps timing out, now delivers its report instead of ending with none.
21
+ - Fixed: A search that cannot be shown to have run is now listed as not searched, and the report is still delivered.
22
+ - Fixed: Reports can set aside an identical mark registered for unrelated goods, recording the reason in the audit workbook instead of listing it as a finding.
23
+
3
24
  ## 0.5.0-beta.2
4
25
 
5
26
  ### Patch Changes
@@ -1129,24 +1129,32 @@ export const FLOORS_BY_NAME_HERE = "Every floor here is placed or set aside by n
1129
1129
  export const A_CROWDS_ONE_ROW = "Read the first page, carry forward any line a lawyer would raise, and set the rest aside in this group's one row with the count: the client is told the field is crowded.";
1130
1130
  export const A_CROWDS_ROW_COVERS_A_READ_FLOOR = "This row covers a floor only once you have read its line and its goods are not the client's goods.";
1131
1131
  export const AN_UNREAD_CROWDS_ONE_ROW = "Carry forward any line a lawyer would raise, and set the rest aside in this list's one row with the count, on the ground that their goods are unreadable and the field is crowded.";
1132
- export const AN_UNREAD_CROWDS_FLOOR = "A floor here is placed or set aside by name on its status or its territory; this row never covers it.";
1133
- export const AN_UNREAD_LISTS_FLOOR = "A floor here is placed, or set aside by name on its status or its territory; any other line may be set aside on the ground that its goods are unknown.";
1132
+ export const AN_UNREAD_CROWDS_FLOOR = "This row never covers a floor. Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full.";
1133
+ export const AN_UNREAD_LISTS_FLOOR = "Place each floor here, or set it aside by name; a goods ground needs the floor's goods read in full. Any other line here may be set aside because its goods are unknown.";
1134
1134
  // What placement does with a line of that list before its fate, by its marker (approved wording, 2026-10-19). Placement's
1135
1135
  // alone, as its other orders are: every other stage keeps the markers as evidence and is not told of a fate.
1136
1136
  export const AN_UNREAD_LINE_IS_CHECKED = 'A line marked "on file, not readable", "cut by the register, no goods word found", "goods held; completeness not established" or "no goods text held; completeness not established" must be checked with band_record before its fate. Read the evidence held for it, keeping its source and goods completeness clear. A line marked "no goods on file", "fetch failed, no goods on file" or "the register\'s record lists no goods" has no goods text on file. Missing goods do not establish that the goods are unrelated.';
1137
1137
  export const PLACEMENT_LINE_ORDERS = Object.freeze([FLOORS_BY_NAME_HERE, A_CROWDS_ONE_ROW, A_CROWDS_ROW_COVERS_A_READ_FLOOR, AN_UNREAD_CROWDS_ONE_ROW, AN_UNREAD_CROWDS_FLOOR, AN_UNREAD_LISTS_FLOOR, AN_UNREAD_LINE_IS_CHECKED]);
1138
+ // AND THE SIZE RULE'S ORDERS TO READ (ruled 2026-10-27). A list's size sentence tells placement how much of it to read,
1139
+ // and only placement owes the lines that read. Every other stage is served the shape without these orders too: the
1140
+ // writer is told the groups are a way to find evidence, never a list it must read to completion. Each list's count,
1141
+ // what it holds and the fact that a crowded one prints its first page stay.
1142
+ export const READ_THEM_ALL = "Fifty? Read them all.";
1143
+ export const READ_EVERY_LINE = ": read every line.";
1144
+ export const READ_THE_LIVE_ONES_FIRST = "A few hundred? Read the live ones in the ordered classes first.";
1138
1145
  const SHAPE_LISTS = ["a", "b", "c", "d", "rest", "no_goods"];
1139
1146
  const pageEveryOtherLine = (g) => `; page every other line with ${groupPageCall(g)}`;
1140
1147
  const pageWhereALineNeedsIt = (g) => `; page with ${groupPageCall(g)} only where a line needs reading`;
1141
1148
  const pageTheFloorsLeft = (g) => `Where more floors are counted than this page shows, page the rest with ${groupPageCall(g)}; they come first.`;
1142
1149
  /**
1143
- * The shape as every stage but placement is served it: placement's orders and its group-page commands removed,
1150
+ * The shape as every stage but placement is served it: placement's orders, its size rule's orders to read and its group-page commands removed,
1144
1151
  * each with the space before it, inside its line, so no line moves and the line each listed mark is printed on
1145
1152
  * still holds. A crowded group's "The first N lines are below" stays, as a fact about what is printed. PURE.
1146
1153
  */
1147
1154
  export function withoutPlacementOrders(text) {
1148
1155
  let t = String(text ?? "");
1149
- for (const order of [...PLACEMENT_GROUP_ORDERS, ...PLACEMENT_LINE_ORDERS, ...SHAPE_LISTS.map(pageTheFloorsLeft)]) t = t.split(` ${order}`).join("").split(order).join("");
1156
+ for (const order of [...PLACEMENT_GROUP_ORDERS, ...PLACEMENT_LINE_ORDERS, ...SHAPE_LISTS.map(pageTheFloorsLeft), READ_THEM_ALL, READ_THE_LIVE_ONES_FIRST]) t = t.split(` ${order}`).join("").split(order).join("");
1157
+ t = t.split(READ_EVERY_LINE).join(".");
1150
1158
  for (const g of SHAPE_LISTS) t = t.split(pageEveryOtherLine(g)).join("").split(pageWhereALineNeedsIt(g)).join("");
1151
1159
  return t;
1152
1160
  }
@@ -1200,16 +1208,16 @@ function renderGroupsMd(md, groups, rows = []) {
1200
1208
  md.push(`### ${heading(g)}`, "");
1201
1209
  if (!grp.lines) { md.push("(none)", ""); continue; }
1202
1210
  const crowded = grp.records >= groups.crowded_at;
1203
- const readA = "These are the identical and near-identical marks live in the ordered classes for the client's goods: read every line.";
1211
+ const readA = `These are the identical and near-identical marks live in the ordered classes for the client's goods${READ_EVERY_LINE}`;
1204
1212
  md.push(g === "a"
1205
1213
  ? (crowded
1206
1214
  ? `${count(grp)}. ${readA} ${firstPage}${pageEveryOtherLine(g)}. ${FLOORS_BY_NAME_HERE}`
1207
- : grp.lines <= READ_ALL_LINES ? `${count(grp)}. Fifty? Read them all.` : `${count(grp)}. ${readA}`)
1215
+ : grp.lines <= READ_ALL_LINES ? `${count(grp)}. ${READ_THEM_ALL}` : `${count(grp)}. ${readA}`)
1208
1216
  : crowded
1209
1217
  ? `${count(grp)}. A thousand or more? The field is crowded. These are ${crowd[g]}. ${A_CROWDS_ONE_ROW} ${firstPage}${pageWhereALineNeedsIt(g)}.${
1210
1218
  g === "b" ? ` ${A_CROWDS_ROW_COVERS_A_READ_FLOOR} ${pageTheFloorsLeft(g)}` : ""}`
1211
1219
  : grp.lines <= READ_ALL_LINES
1212
- ? `${count(grp)}. Fifty? Read them all.`
1220
+ ? `${count(grp)}. ${READ_THEM_ALL}`
1213
1221
  : `${count(grp)}. ${SORT_THE_NEAR_ONES}`, "");
1214
1222
  md.push(...head);
1215
1223
  for (const l of crowded ? grp.list.slice(0, groups.first_page) : grp.list) { rows.push([md.length, l.key]); md.push(row(l)); }
@@ -1223,7 +1231,7 @@ function renderGroupsMd(md, groups, rows = []) {
1223
1231
  md.push(`**The rest: ${count(rest)}${outside}** — every record in none of (a) to (d) above. ${!rest.lines ? ONE_SET_ASIDE_ROW
1224
1232
  : restCrowded
1225
1233
  ? `A thousand or more? The field is crowded. ${A_CROWDS_ONE_ROW} ${firstPage}${pageWhereALineNeedsIt("rest")}.`
1226
- : `${rest.lines <= READ_ALL_LINES ? "Fifty? Read them all." : SORT_THE_NEAR_ONES} ${ONE_SET_ASIDE_ROW}`}`);
1234
+ : `${rest.lines <= READ_ALL_LINES ? READ_THEM_ALL : SORT_THE_NEAR_ONES} ${ONE_SET_ASIDE_ROW}`}`);
1227
1235
  if (rest.lines) {
1228
1236
  md.push("", ...head);
1229
1237
  for (const l of restCrowded ? rest.list.slice(0, groups.first_page) : rest.list) { rows.push([md.length, l.key]); md.push(row(l)); }
@@ -1231,8 +1239,8 @@ function renderGroupsMd(md, groups, rows = []) {
1231
1239
  if (groups.no_goods.lines) {
1232
1240
  // The lines the goods test could not place, read by the same size rule (ruled 2026-10-12): under a thousand
1233
1241
  // records, a fate per line by name, opened where goods are on file; at a thousand or more, the first page,
1234
- // and one row on the rest whose goods are unreadable in a crowded field. A floor is never set aside on
1235
- // its goods, so a floor here is placed or set aside on its status or territory.
1242
+ // and one row on the rest whose goods are unreadable in a crowded field. A floor is set aside on its goods only
1243
+ // once they are read in full, which unknown goods never are, so a floor here is placed or set aside by name.
1236
1244
  const ng = groups.no_goods;
1237
1245
  const crowded = ng.records >= groups.crowded_at;
1238
1246
  const lead = tested.source === "none"
@@ -1240,7 +1248,7 @@ function renderGroupsMd(md, groups, rows = []) {
1240
1248
  : `**Goods the test could not read: ${count(ng)}.**`;
1241
1249
  md.push("", crowded
1242
1250
  ? `${lead} A thousand or more? ${firstPage}, the floors first, then the live ones in the ordered classes. ${AN_UNREAD_CROWDS_ONE_ROW} ${AN_UNREAD_CROWDS_FLOOR} ${pageTheFloorsLeft("no_goods")}`
1243
- : `${lead} ${ng.lines <= READ_ALL_LINES ? "Fifty? Read them all." : "A few hundred? Read the live ones in the ordered classes first."} ${A_FATE_BY_NAME}${tested.source === "none" ? "" : ` ${AN_UNREAD_LINE_IS_CHECKED}`} ${AN_UNREAD_LISTS_FLOOR}`, "");
1251
+ : `${lead} ${ng.lines <= READ_ALL_LINES ? READ_THEM_ALL : READ_THE_LIVE_ONES_FIRST} ${A_FATE_BY_NAME}${tested.source === "none" ? "" : ` ${AN_UNREAD_LINE_IS_CHECKED}`} ${AN_UNREAD_LISTS_FLOOR}`, "");
1244
1252
  md.push(...head);
1245
1253
  for (const l of crowded ? ng.list.slice(0, groups.first_page) : ng.list) { rows.push([md.length, l.key]); md.push(row(l)); }
1246
1254
  }
@@ -530,9 +530,9 @@ export const INNER_CODES = Object.freeze([
530
530
  // ONE CODE, TWO TOKENS, and the split is by `cause` rather than by code: verify.mjs peels the
531
531
  // `axis_invalid` cause into its own family before counting the rest. A ruling naming only
532
532
  // `coverage_no_status` would be true of most `no_status` records and false of the ones that matter most.
533
- { code: "no_status", mints: ["driver/coverage-form.mjs:920"], rollsUpTo: ["coverage_no_status", "coverage_form_axis_invalid"],
533
+ { code: "no_status", mints: ["driver/coverage-form.mjs:939"], rollsUpTo: ["coverage_no_status", "coverage_form_axis_invalid"],
534
534
  why: "Two composites, split on the record's `cause`: coverageFormFail() in verify.mjs for cause `axis_invalid`, and the same function for the rest. NOT the `@returns` annotation of findCoverageFormViolations() in coverage-form.mjs — that is the annotation, not the mint." },
535
- { code: "engine_vocabulary", mints: ["driver/coverage-form.mjs:910"], rollsUpTo: ["coverage_form_engine_vocabulary"],
535
+ { code: "engine_vocabulary", mints: ["driver/coverage-form.mjs:929"], rollsUpTo: ["coverage_form_engine_vocabulary"],
536
536
  why: "#669 — the seat wrote an engine token into the `reason` sentence that reaches the reader's page. Checked on settled rows too, because a row the seat considers finished is exactly the one whose sentence gets printed. Namespaced by coverageFormFail() in verify.mjs; the bare code names a row, never a stage." },
537
537
 
538
538
  // ── ONE CODE, TWO MODULES ────────────────────────────────────────────────────────────────────────
@@ -544,7 +544,7 @@ export const INNER_CODES = Object.freeze([
544
544
  // failure tokens" already states the rule this row records: `coverage_form_damaged`, never a bare
545
545
  // `form_damaged`.
546
546
  { code: "form_damaged",
547
- mints: ["driver/connotation-search.mjs:2002", "driver/connotation-search.mjs:2078", "driver/coverage-form.mjs:902"],
547
+ mints: ["driver/connotation-search.mjs:2002", "driver/connotation-search.mjs:2078", "driver/coverage-form.mjs:921"],
548
548
  rollsUpTo: ["connotation_form_damaged", "coverage_form_damaged"],
549
549
  why: "Minted in two lanes and namespaced per lane: connotationDispositionFail() in verify.mjs for the meaning sweep, coverageFormFail() in verify.mjs for the register digest. The namespacing is what keeps them apart — see verify.mjs's block headed \"the register coverage-form failure tokens\"." },
550
550
  ]);
@@ -32,6 +32,7 @@ import { COVERAGE_STATUSES, COVERAGE_FORM_NAME, REGISTER_AXES } from "./coverage
32
32
  import { coverageFormSidecarName, parseCoverageForm } from "./coverage-form.mjs";
33
33
  import { capabilitiesFor } from "./register-capabilities.mjs";
34
34
  import { readWithheldFamilies, splitWaitingFamilies } from "./withheld-families.mjs";
35
+ import { slicesTakenOutByAxis } from "../shared/slices-taken-out.mjs"; // the slices the searches record took out of the band
35
36
 
36
37
  export { COVERAGE_FORM_NAME };
37
38
 
@@ -150,7 +151,8 @@ export function coverageFormInput(runDir) {
150
151
  return { input: { skeleton: exec.skeleton, plan, bandBlocksByAxis, deferredReasons, activeAxes,
151
152
  bandsUnreadable, orderedTerritories, capabilities, unknownAxisUnits,
152
153
  awaiting: Array.isArray(exec.awaiting) ? exec.awaiting : [], withheld: readWithheldFamilies(runDir),
153
- mainMarkets: Array.isArray(exec.main_markets) ? exec.main_markets : [] }, absent: null };
154
+ mainMarkets: Array.isArray(exec.main_markets) ? exec.main_markets : [],
155
+ takenOut: Object.fromEntries(slicesTakenOutByAxis(runDir)) }, absent: null };
154
156
  }
155
157
 
156
158
  /**
@@ -353,10 +353,14 @@ export const DEFERRED_OPEN_BECAUSE = Object.freeze({
353
353
  });
354
354
  const deferredOpenBecause = (block, receiptReason) => block?.gateway_timeout === true ? DEFERRED_OPEN_BECAUSE.timeout
355
355
  : isProviderHardErrorReason(receiptReason) ? DEFERRED_OPEN_BECAUSE.hardError : DEFERRED_OPEN_BECAUSE.capability;
356
+ // A SLICE THE RUN TOOK OUT OF THE BAND (the searches not completed, parts-not-completed.mjs) is open for the reason its
357
+ // record gives, which is the shipped line the reader gets for it (ruled 2026-10-27): the provider could express it, and
358
+ // what is missing is the evidence that it ran. A taken-out slice with no qid has no plan row to defer it, so it gets a
359
+ // row of its own, keyed on its query.
356
360
 
357
361
  export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null,
358
362
  bandBlocksByAxis = {}, deferredReasons = {}, bandsUnreadable = [],
359
- orderedTerritories = [], capabilities = null, awaiting = [], withheld = {}, mainMarkets = [] } = {}) {
363
+ orderedTerritories = [], capabilities = null, awaiting = [], withheld = {}, mainMarkets = [], takenOut = {} } = {}) {
360
364
  const skel = Array.isArray(skeleton) ? skeleton.filter((s) => s && typeof s === "object") : [];
361
365
  // WHAT WAS RETRIEVED UNDER EACH CROWD (register-plan.mjs, crowdNarrowing), worked out from the same blocks these rows
362
366
  // are built from and carried on its row as data: a narrowed list retrieved in full (and which), none, or for the
@@ -423,6 +427,8 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
423
427
  status: null, reason: null,
424
428
  });
425
429
  }
430
+ const out = Array.isArray(takenOut?.[axis]) ? takenOut[axis] : [];
431
+ const takenReason = new Map(out.filter((x) => x?.qid && x?.reason).map((x) => [String(x.qid), String(x.reason)]));
426
432
  for (const qid of deferredQids) {
427
433
  rows.push({
428
434
  row_id: shortId("CD", `deferred:${qid}`),
@@ -433,7 +439,20 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
433
439
  // It was never searched, or it was asked and never answered, and nothing in this run asks it again, so a clean
434
440
  // claim over it is not a judgment the seat gets to make.
435
441
  open: true,
436
- open_because: deferredOpenBecause(blockByQid.get(qid), deferredReasons?.[qid]),
442
+ open_because: takenReason.get(qid) ?? deferredOpenBecause(blockByQid.get(qid), deferredReasons?.[qid]),
443
+ status: null, reason: null,
444
+ });
445
+ }
446
+ for (const x of out) {
447
+ if (x?.qid || !x?.query || !x?.reason) continue;
448
+ rows.push({
449
+ row_id: shortId("CD", `deferred:${axis}:query:${x.query}`),
450
+ axis, kind: "deferred",
451
+ unit: unitLabel(axis, { term: String(x.query) }),
452
+ qid: null, query: String(x.query),
453
+ receipt_reason: String(x.reason),
454
+ open: true,
455
+ open_because: String(x.reason),
437
456
  status: null, reason: null,
438
457
  });
439
458
  }
@@ -23,6 +23,7 @@
23
23
  // `validators` at module scope, so verify importing stages would TDZ-crash whenever verify loads
24
24
  // first; stages.mjs re-exports both names, keeping every existing import site working).
25
25
 
26
+ import { plainDeferralReason } from "./deferral-row.mjs";
26
27
  export const REGISTER_AXES = ["saturation-probe", "primary-sweep", "transliteration-numeric", "incumbent-class"];
27
28
 
28
29
  // ── — THE READER'S NAME FOR AN AXIS, EMITTED AT SOURCE ──────────────────────────────────────────
@@ -347,13 +348,18 @@ export function coverageLedgerTableRows(rows, { reasonMax = 180 } = {}) {
347
348
  // confirmed-clean rows on tainted material axes move; coverage-limited/deferred rows and non-material
348
349
  // axes (saturation-probe) are untouched; an empty taint set is a no-op. PURE; the caller pre-reads the
349
350
  // taint verdicts (pipeline loadCoverageLedger → readRegisterTaint).
351
+ // The reason a tainted axis's withdrawn clean claim carries: the shipped gap sentence (deferral-row.mjs).
352
+ export const TAINTED_CLEAN_REASON = plainDeferralReason("unfinished");
353
+
350
354
  export function applyTaintDeferred(rows, taintedAxes) {
351
355
  const tainted = new Set((taintedAxes ?? []).map((a) => String(a).toLowerCase()));
352
356
  if (!tainted.size) return rows ?? [];
353
357
  const material = (a) => !NON_MATERIAL_AXES.includes(String(a ?? "").toLowerCase());
358
+ // The pass that claimed clean was killed before it finished reading, so the claim is withdrawn whole: the row is open,
359
+ // with the shipped reason a reader gets for a part not completed, and none of the claim's words.
354
360
  return (rows ?? []).map((r) =>
355
361
  (r && r.status === "confirmed-clean" && tainted.has(String(r.axis ?? "").toLowerCase()) && material(r.axis))
356
- ? { ...r, status: "deferred", reason: `${String(r.reason ?? "").trim()}${r.reason ? " — " : ""}timeout-tainted pass: self-reported clean downgraded pending re-run`.trim() }
362
+ ? { ...r, status: "deferred", reason: TAINTED_CLEAN_REASON }
357
363
  : r);
358
364
  }
359
365
 
@@ -117,6 +117,13 @@ export const DECLINATION_REASONS = Object.freeze({
117
117
  gloss: "an off-field name that is not a major brand, an active dispute or proceeding, or a "
118
118
  + "well-known enforcer — omitted, never listed for completeness",
119
119
  },
120
+ // A COMPARISON OF MARKS (ruled 2026-10-27): the writer may set aside a near spelling on its sign, as the write-up
121
+ // may, with the write-up's own gloss. It is refused on the applied-for mark itself, which no comparison sets aside.
122
+ sign: {
123
+ discretionary: true,
124
+ rules: 'synthesis-rules.md, "The consumer confusion test — the governing gate"',
125
+ gloss: "a near spelling a buyer in this market could not take for the mark, by sound, by look or by meaning",
126
+ },
120
127
  "not-worth-the-line": {
121
128
  discretionary: true,
122
129
  rules: 'synthesis-rules.md, "Promote-or-omit"',
@@ -206,7 +213,7 @@ export function contradictionFor(reason, row, scope) {
206
213
  // class is closed to the entry that holds it. Each record is judged on its own mark, status and classes.
207
214
  if (Array.isArray(row?.members) && row.members.length) {
208
215
  for (const m of row.members) {
209
- const clash = contradictionFor(reason, { mark: m?.mark ?? row.mark, status: m?.status ?? null, classes: m?.classes ?? [] }, scope);
216
+ const clash = contradictionFor(reason, { mark: m?.mark ?? row.mark, status: m?.status ?? null, classes: m?.classes ?? [], goods: m?.goods ?? null }, scope);
210
217
  if (clash) return clash;
211
218
  }
212
219
  return "";
@@ -218,14 +225,26 @@ export function contradictionFor(reason, row, scope) {
218
225
  // it switched the identical-mark refusal below off on exactly those runs.
219
226
  const marks = Array.isArray(scope?.marks) ? scope.marks : (scope?.marks ? [scope.marks] : []);
220
227
 
228
+ // ── AN IDENTICAL MARK IN A FILED CLASS MAY BE DECLINED ON ITS GOODS, AND ON NOTHING ELSE DISCRETIONARY ──
229
+ //
230
+ // Ruled 2026-10-27: identical spelling and a shared class no longer bar a supported judgment that the registered
231
+ // goods are unrelated. This refused every discretionary token on such a record, so on a test run 29 goods
232
+ // decisions were refused and every one came back as a finding. A goods decision now stands where the run holds the
233
+ // record's goods complete (goods-evidence.mjs), because cut, unknown or missing goods establish nothing. The
234
+ // record's field, the report's length and a comparison with the applied-for mark itself are still refused.
221
235
  if (isIdenticalToAppliedMark(row?.mark, marks) && isLive(row?.status) && shared.length) {
222
- return `this record's mark is IDENTICAL to the applied-for mark under the house normalizer, its `
223
- + `status is ${String(row?.status ?? "").trim() || "live"}, and it sits in class ${shared.join(", ")} `
224
- + `— one of the matter's own filed classes. synthesis-rules.md:167 orders that an on-point `
225
- + `identical mark in the relevant class is never dropped, and says so "regardless of filer `
226
- + `profile". Deliver it and rate it; a Stage-2 mitigant is a rating input, never a reason to omit. `
227
- + `If it is the applicant's own right or already delivered under another record, say so with `
228
- + `own-right or duplicate-of-delivered.`;
236
+ const facts = `this record's mark is IDENTICAL to the applied-for mark, its status is `
237
+ + `${String(row?.status ?? "").trim() || "live"}, and it is in filed class ${shared.join(", ")}`;
238
+ if (reason === "unrelated-goods") {
239
+ if (row?.goods === "complete") return "";
240
+ return `${facts}. The run holds its goods as ${String(row?.goods ?? "").trim() || "not recorded"}, not in full, `
241
+ + `and incomplete goods cannot show the goods are unrelated. Deliver and rate it, or use own-right or `
242
+ + `duplicate-of-delivered if one is true.`;
243
+ }
244
+ return `${facts}. Such a mark is never declined for its field, for want of room, or on its sign. Deliver and `
245
+ + `rate it; a Stage-2 mitigant goes into the rating. If its registered goods, read in full, do not meet the `
246
+ + `matter's goods, decline it as unrelated-goods and say so in the ground. If it is the applicant's own right `
247
+ + `or already delivered, use own-right or duplicate-of-delivered.`;
229
248
  }
230
249
 
231
250
  // ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (ruling 2026-09-07) ─────────────────
@@ -250,14 +269,12 @@ export function contradictionFor(reason, row, scope) {
250
269
  // It does effectively retire this token for live in-class records, because the sweep is already scoped
251
270
  // to the instructed classes. That was put to the owner as the consequence and ruled anyway.
252
271
  if (reason === "not-worth-the-line" && isLive(row?.status) && shared.length) {
253
- return `this record is ${String(row?.status ?? "").trim() || "live"} and sits in class `
254
- + `${shared.join(", ")} — one of the matter's own instructed classes. "not worth the line" is a `
255
- + `statement about the report's budget, not about this mark, and a live registration in an `
256
- + `instructed class is never omitted for want of room. Either carry it, or decline it on something `
257
- + `true of THIS MARK: unrelated-goods if its goods do not meet the matter's, off-field-not-major `
258
- + `if it is off-field and not a major brand, an active dispute or a well-known enforcer, `
259
- + `own-right if it belongs to the applicant, duplicate-of-delivered if the same right is already `
260
- + `reported. Say what makes this mark distant, or give it its line.`;
272
+ return `this record is ${String(row?.status ?? "").trim() || "live"} in instructed class ${shared.join(", ")}. `
273
+ + `A live mark in an instructed class is never declined for want of room. Deliver it, or decline it on a fact `
274
+ + `about this mark: unrelated-goods if its goods do not meet the matter's; off-field-not-major if it is `
275
+ + `off-field and not a major brand, an active dispute or a well-known enforcer; sign if a buyer in this market `
276
+ + `could not take its spelling for the mark; own-right if it belongs to the applicant; duplicate-of-delivered `
277
+ + `if the same right is already reported.`;
261
278
  }
262
279
 
263
280
  // THERE IS NO CLASS-OVERLAP REFUSAL, AND THE FIRST DRAFT HAD ONE. It refused `unrelated-goods` and
@@ -171,6 +171,12 @@ function register(runDir, log) {
171
171
  const open = readPartNotCompleted(runDir, "assessment");
172
172
  if (open && ["placement-inquiry", "register-digest"].includes(open.stage))
173
173
  failed.push([{ fail: `${open.stage} did not complete: ${open.reason ?? "no cause recorded"}` }, "the register write-up"]);
174
+ // The searches a check could not prove ran, taken out of the band and delivered as open rows (parts-not-completed.mjs).
175
+ const searches = readPartNotCompleted(runDir, "searches");
176
+ if (searches) failed.push([{ fail: [
177
+ (searches.slices ?? []).length ? `${searches.slices.length} slice(s) taken out of the band` : "",
178
+ (searches.disclosed_axes ?? []).length ? `the pass on ${searches.disclosed_axes.join(", ")} timed out, read as it stands with its clean claims open` : "",
179
+ ].filter(Boolean).join("; ") + `: ${searches.reason ?? "no cause recorded"}` }, "the searches"]);
174
180
  let reopen = null;
175
181
  try { reopen = JSON.parse(readFileSync(driverDir(runDir, "frame-reopen.json"), "utf8")); } catch { reopen = null; }
176
182
  if (reopen?.dominant_element_gap === true && reopen?.domClosed !== true)
@@ -280,9 +280,9 @@ function bandShape(params) {
280
280
  ...(read ? { served_lines: read.keys[part - 1], group_parts: read.groupParts } : {}) });
281
281
  const head = format === "json"
282
282
  ? `[band_shape part ${part}/${total} of the json; concatenate the parts before parsing it.]`
283
- : `[band_shape part ${part}/${total}. Read each group by the size sentence at its head; the next part continues the groups.]`;
283
+ : `[band_shape part ${part}/${total}. ${GROUP_PAGES ? "Read each group by the size sentence at its head; the next part continues the groups." : "The next part continues the groups."}]`;
284
284
  const tail = part < total
285
- ? `\n[end of part ${part}/${total} — call band_shape again with {"part": ${part + 1}} for the next part]`
285
+ ? `\n[end of part ${part}/${total} — part ${part + 1} is available with {"part": ${part + 1}}]`
286
286
  : `\n[end of the shape]`;
287
287
  return head + "\n" + parts[part - 1] + tail;
288
288
  }
@@ -351,7 +351,7 @@ function groupLookup(params) {
351
351
  const body = () => JSON.stringify({
352
352
  group: g, lines_total: lines.length, returned: page.length, ...(offset ? { offset } : {}),
353
353
  ...(lines.length > offset + page.length ? { next_offset: offset + page.length,
354
- note: `call again with offset: ${offset + page.length} for the next lines of group ${g}` } : {}),
354
+ note: `the next lines of group ${g} are available with offset: ${offset + page.length}` } : {}),
355
355
  lines: page.map(({ _key, ...l }) => l),
356
356
  }, null, 2);
357
357
  let out = body();
@@ -440,8 +440,8 @@ function bandLookup(params = {}) {
440
440
  const body = () => JSON.stringify({
441
441
  matched, returned: hits.length, ...(offset ? { offset } : {}),
442
442
  ...(matched > offset + hits.length ? { next_offset: offset + hits.length } : {}),
443
- ...(matched > offset + hits.length ? { note: `capped at ${hits.length} — call again with offset: ${offset + hits.length} for the next page; the count above is the full match` } : {}),
444
- ...(byteCapped ? { byte_cap_note: `${byteCapped} matched record(s) withheld to fit one tool response — call again with offset: ${offset + hits.length} for the rest` } : {}),
443
+ ...(matched > offset + hits.length ? { note: `capped at ${hits.length} — the next page is available with offset: ${offset + hits.length}; the count above is the full match` } : {}),
444
+ ...(byteCapped ? { byte_cap_note: `${byteCapped} matched record(s) withheld to fit one tool response — the rest are available with offset: ${offset + hits.length}` } : {}),
445
445
  records: hits,
446
446
  ...(crowds.length ? { matching_crowds: crowds, crowd_note: "these matching slices were NOT enumerated — records under them may exist that no lookup can return; weigh the descriptor, never treat this result as their clean"
447
447
  + (refusedCrowds ? ` — and ${refusedCrowds} of them was/were NEVER ANSWERED at all (see "refusal"): no count exists for those, so they are unsearched rather than empty, and nothing about them can be read off a zero` : "")
@@ -515,7 +515,7 @@ function bandRecord(params = {}) {
515
515
  logRead("band_record", { record_id: uri, part, parts: total }, { ok: true, bytes: parts[part - 1].length, total_bytes: text.length, ...resolution, ...served.audit });
516
516
  const head = `[band_record part ${part}/${total}. This record is served in ${total} parts; the next part continues it.]`;
517
517
  const tail = part < total
518
- ? `\n[end of part ${part}/${total} — call band_record again with {"record_id": "${uri}", "part": ${part + 1}} for the next part]`
518
+ ? `\n[end of part ${part}/${total} — part ${part + 1} is available with {"record_id": "${uri}", "part": ${part + 1}}]`
519
519
  : `\n[end of the record]`;
520
520
  return head + "\n" + parts[part - 1] + tail;
521
521
  }
@@ -537,7 +537,7 @@ function bandRecord(params = {}) {
537
537
  const part = Number.isInteger(wanted) && wanted >= 1 && wanted <= parts.length ? wanted : 1;
538
538
  logRead("band_record", { record_id: uri }, { ok: true, bytes: text.length, via: "ledger", part, parts: parts.length, ...served.audit });
539
539
  return { type: "text", text: `[record ${uri} — part ${part}/${parts.length}]\n` + parts[part - 1]
540
- + (part < parts.length ? `\n[end of part ${part}/${parts.length} — call band_record again with {"record_id": "${uri}", "part": ${part + 1}}]` : "") };
540
+ + (part < parts.length ? `\n[end of part ${part}/${parts.length} — part ${part + 1} is available with {"record_id": "${uri}", "part": ${part + 1}}]` : "") };
541
541
  }
542
542
 
543
543
  // A GENUINE miss — neither materialised NOR in this run's ledger. Now the sentence below is true.
@@ -581,7 +581,7 @@ function bandRecord(params = {}) {
581
581
  ...(parts.length > 1 ? { part, parts: parts.length } : {}), ...(failed ? { fetch_failed: true } : {}) });
582
582
  if (parts.length === 1) return { isError: true, text };
583
583
  return { isError: true, text: `[record ${uri} — part ${part}/${parts.length}]\n` + parts[part - 1]
584
- + (part < parts.length ? `\n[end of part ${part}/${parts.length} — call band_record again with {"record_id": "${uri}", "part": ${part + 1}}]` : "") };
584
+ + (part < parts.length ? `\n[end of part ${part}/${parts.length} — part ${part + 1} is available with {"record_id": "${uri}", "part": ${part + 1}}]` : "") };
585
585
  }
586
586
 
587
587
  // The band row a record id names, from the band file, read once per server. null where the band holds none.
@@ -610,10 +610,10 @@ const GROUP_PAGES = process.env.CLEAROTRON_BAND_GROUPS === "1";
610
610
  // served the shape without them (the owner's ruling, 2026-10-14). Each is removed with the space before it, inside
611
611
  // its line, so no line moves and the line each listed mark is printed on, which `linesByPart` reads, still holds.
612
612
  // (band-shape.mjs `withoutPlacementOrders` removes them, with the shape's other placement-only orders and its group-page commands.)
613
- const LOOKUP_DESCRIPTION = "Look up enumerated records in this run's frozen register band (read-only; logged to the reading audit). Filters AND together: record_id (exact uri), owner (substring), nice_class, tier (identical|near-identical|same-family|other|unclassifiable — classified against the shape's frozen targets), qid (exact plan-entry id — the plan-execution ledger join; a record surfaced by several slices matches under EVERY one of them), query (the _query slice that surfaced the record), text (substring of mark_text), status (substring), live_only, goods (words: records whose goods contain any of them, as whole words, singular or plural; a record with no goods on file matches too), group (a, b, c, d, rest or no_goods: that list's lines in the shape's order, one per mark of an owner), limit (default 20, max 100), offset (default 0: where in the matching records or lines to start — a capped answer names the next offset, so a large band or group is read page by page). On each record served, `goods_words` gives the client's goods words its goods hold, as the shape tests them (k of N: the words), or null where its goods cannot be read; `goods_from` says what those goods rest on: official record; official record, cut by the register; official record, completeness not established; official record, no goods listed; search answer; search answer, cut by the register; search answer, completeness not established; fetch failed; or nothing held. The goods text is in band_record. Matching un-enumerated crowd descriptors ride along so a lookup can never mistake a counted-only zone for a clean.";
613
+ const LOOKUP_DESCRIPTION = "Look up enumerated records in this run's frozen register band (read-only; logged to the reading audit). Filters AND together: record_id (exact uri), owner (substring), nice_class, tier (identical|near-identical|same-family|other|unclassifiable — classified against the shape's frozen targets), qid (exact plan-entry id — the plan-execution ledger join; a record surfaced by several slices matches under EVERY one of them), query (the _query slice that surfaced the record), text (substring of mark_text), status (substring), live_only, goods (words: records whose goods contain any of them, as whole words, singular or plural; a record with no goods on file matches too), group (a, b, c, d, rest or no_goods: that list's lines in the shape's order, one per mark of an owner), limit (default 20, max 100), offset (default 0: where in the matching records or lines to start — a capped answer names the offset where the next page starts). On each record served, `goods_words` gives the client's goods words its goods hold, as the shape tests them (k of N: the words), or null where its goods cannot be read; `goods_from` says what those goods rest on: official record; official record, cut by the register; official record, completeness not established; official record, no goods listed; search answer; search answer, cut by the register; search answer, completeness not established; fetch failed; or nothing held. The goods text is in band_record. Matching un-enumerated crowd descriptors ride along so a lookup can never mistake a counted-only zone for a clean.";
614
614
  const LOOKUP_GROUP_CLAUSE = "group (a, b, c, d, rest or no_goods: that list's lines in the shape's order, one per mark of an owner), ";
615
- const LOOKUP_OFFSET_GROUPS = "offset (default 0: where in the matching records or lines to start — a capped answer names the next offset, so a large band or group is read page by page)";
616
- const LOOKUP_OFFSET_BAND = "offset (default 0: where in the matching records to start — a capped answer names the next offset, so a large band is read page by page)";
615
+ const LOOKUP_OFFSET_GROUPS = "offset (default 0: where in the matching records or lines to start — a capped answer names the offset where the next page starts)";
616
+ const LOOKUP_OFFSET_BAND = "offset (default 0: where in the matching records to start — a capped answer names the offset where the next page starts)";
617
617
  /** The lookup's description as a stage is offered it: whole where it reads the groups, without them elsewhere. PURE. */
618
618
  const lookupDescription = (groupPages) => (groupPages ? LOOKUP_DESCRIPTION
619
619
  : LOOKUP_DESCRIPTION.replace(LOOKUP_GROUP_CLAUSE, "").replace(LOOKUP_OFFSET_GROUPS, LOOKUP_OFFSET_BAND));
@@ -623,7 +623,7 @@ serve({
623
623
  tools: [
624
624
  {
625
625
  name: "band_shape",
626
- description: "The deterministic shape of this run's merged register band: totals, mechanical tiers, the floors (every live in-class identical/near-identical record), each marked on its line in the band in the lawyer's order, class/status/registry/recency census, owner concentrations, crowd descriptors (with crowd-context joins) and blind spots. Read this FIRST, then pull what it names via band_lookup/band_record. A large shape is served in parts, each naming the next call. Every call is logged to the run's reading audit.",
626
+ description: "The deterministic shape of this run's merged register band: totals, mechanical tiers, the floors (every live in-class identical/near-identical record), each marked on its line in the band in the lawyer's order, class/status/registry/recency census, owner concentrations, crowd descriptors (with crowd-context joins) and blind spots. What it names can be looked up with band_lookup and band_record. A large shape is served in parts, and the end of each part names the next one. Every call is logged to the run's reading audit.",
627
627
  inputSchema: { type: "object", properties: { format: { type: "string", enum: ["md", "json"], description: "md (default, readable) or json (machine shape)" }, part: { type: "number", description: "1-based part number when the shape is served in parts (default 1)" } } },
628
628
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
629
629
  handler: bandShape,
@@ -29,6 +29,7 @@
29
29
  // `match_mode: "phonetic"` on register_search / register_enumerate remains fully available; it is the
30
30
  // PREVIEW of the expansion that this provider cannot give, not phonetic search itself.
31
31
  import { serve } from "./stdio-server.mjs";
32
+ import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
32
33
  import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
33
34
  import {
34
35
  CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doBatchScreen, doEnumerate, doExecutePlan, DEFAULT_BASE,
@@ -43,6 +44,7 @@ const tctx = (kind) => ({
43
44
  agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
44
45
  sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
45
46
  sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
47
+ sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
46
48
  });
47
49
  const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
48
50
  const guard = (fn) => async (args) => {
@@ -275,10 +275,15 @@ export function featuresToml(withheld = []) {
275
275
  // codex's `workspace-write` sandbox confines where a command WRITES and lets it read the whole disk, the
276
276
  // install's settings files and the program's own sign-in included. A permission profile confines reading
277
277
  // too (the vendor's Permissions guide, a beta feature since 0.138.0; the floor was 0.154.0 when this was written and is 0.159.0 now, for the judgment tier's model — driver.config.mjs): a stage's
278
- // commands read the platform's own paths, the instruction trees and the temp folders, and write the run
279
- // folder, their working folder and the temp folders. Nothing else is readable, so a page a stage fetched
280
- // cannot get a command to print the account's files. The temp folders stay writable because
281
- // `workspace-write` let commands use them and stages write their output through the shell.
278
+ // commands read the platform's own paths and the instruction trees, and write the run folder, their
279
+ // working folder and their Codex home's working folder. Nothing else is readable, so a page a stage
280
+ // fetched cannot get a command to print the account's files.
281
+ //
282
+ // NO SHARED TEMP FOLDER. The profile granted `:tmpdir` and `:slash_tmp`, which every stage of every run on
283
+ // the machine shares, and a later run's commands read what an earlier run left in `/tmp` (measured on codex
284
+ // 0.162.0 with that profile: a command read a file planted in `/tmp` and wrote another beside it). The
285
+ // turn's TMPDIR, TMP and TEMP now name its home's working folder (openai-agent.mjs, `withWorkFolder`), and
286
+ // this grants that folder, named, in their place.
282
287
  //
283
288
  // ONLY WITH THE SANDBOX ON. Under the bypass codex builds no sandbox, so a profile would enforce nothing;
284
289
  // the caller passes no `fence` then, and the file stays what it was. With the sandbox on the command line
@@ -297,10 +302,10 @@ export function featuresToml(withheld = []) {
297
302
  // wins over the run folder's: measured on codex 0.162.0 with this profile, a write into the run folder
298
303
  // succeeded, a read of `_driver/` succeeded, and a write or an append there was refused.
299
304
  export const FENCE_PROFILE = "clearotron-stage";
300
- export function fenceToml({ runDir = null, readRoots = [] } = {}) {
305
+ export function fenceToml({ runDir = null, workDir = null, readRoots = [] } = {}) {
301
306
  const key = (p) => tomlString(p);
302
- const out = [`[permissions.${FENCE_PROFILE}.filesystem]`,
303
- `":minimal" = "read"`, `":tmpdir" = "write"`, `":slash_tmp" = "write"`];
307
+ const out = [`[permissions.${FENCE_PROFILE}.filesystem]`, `":minimal" = "read"`];
308
+ if (workDir) out.push(`${key(workDir)} = "write"`);
304
309
  for (const r of [...new Set(readRoots.filter(Boolean))]) out.push(`${key(r)} = "read"`);
305
310
  if (runDir) out.push(`${key(runDir)} = "write"`, `${key(driverDir(runDir))} = "read"`);
306
311
  out.push("", `[permissions.${FENCE_PROFILE}.filesystem.":workspace_roots"]`, `"." = "write"`, "");
@@ -7,6 +7,7 @@
7
7
  // session key (tctx.sessionKey) — we thread the run's session key via CLEAROTRON_GATHER_SESSION_KEY so the
8
8
  // $0 provider-usage diff still attributes calls to the run.
9
9
  import { serve } from "./stdio-server.mjs";
10
+ import { sendUntilFromEnv } from "../../../providers/_shared/send-until.mjs";
10
11
  import { crowdNoteFor } from "./crowd-note.mjs"; // how the unit decides on a crowd, beside an answer that carries one
11
12
  import { CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doExpandPhoneme, doBatchScreen, doEnumerate, doExecutePlan } from "../../../providers/corsearch/src/core.js";
12
13
  import { proposeSupplemental } from "./supplemental.mjs";
@@ -18,6 +19,7 @@ const tctx = (kind) => ({
18
19
  agentId: process.env.CLEAROTRON_GATHER_AGENT || "localagent",
19
20
  sessionKey: process.env.CLEAROTRON_GATHER_SESSION_KEY || "", // run session key — telemetry attribution
20
21
  sessionId: process.env.CLEAROTRON_GATHER_SESSION_ID || "",
22
+ sendUntilMs: sendUntilFromEnv(), // this turn's time to stop sending (providers/_shared/send-until.mjs)
21
23
  });
22
24
  const passthrough = (r) => (r && typeof r === "object" ? (r.text ?? JSON.stringify(r)) : String(r));
23
25
  const guard = (fn) => async (args) => {
@@ -83,12 +83,11 @@ serve({
83
83
  description:
84
84
  "Record, per position or page, your decision NOT to deliver something that reached your findings surface. " +
85
85
  "Every position the digest carried to you, and every page the web notes marked as a candidate or conflict, " +
86
- "either becomes a finding " +
87
- "or carries a stated reason it did not — there is no third way out, and one left silent is reported as " +
88
- "a defect of this run. One call carries a batch; the answer tells you what was accepted, " +
86
+ "ends as a finding or as a declination with its reason. One left with neither is reported as a defect " +
87
+ "of this run. One call carries a batch; the answer tells you what was accepted, " +
89
88
  "what was refused and why, and WHICH ROWS STILL CARRY NO DECISION, so you can finish inside this " +
90
- "turn. A refusal is about bookkeeping, never about your legal judgment: a reason is refused only " +
91
- "where it contradicts a fact the job recorded before this run started.",
89
+ "turn. A refusal is about the record's facts, never your legal judgment: a reason is refused only when it " +
90
+ "contradicts the matter's instructions or the record's mark, status, classes or goods as the run holds them.",
92
91
  inputSchema: { type: "object", required: ["declinations"], properties: {
93
92
  declinations: {
94
93
  type: "array",