clearotron 0.5.0-beta.3 → 0.5.0-beta.5
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.
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/build-info.json +2 -2
- package/docs/architecture/03-run-lifecycle.md +6 -0
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/docs/architecture/05-config-governance.md +1 -1
- package/driver/CHANGELOG.md +22 -0
- package/driver/band-shape.mjs +15 -7
- package/driver/contract-e3-backlog.mjs +6 -6
- package/driver/coverage-form.mjs +1 -1
- package/driver/engine/mcp/band-server.mjs +12 -12
- package/driver/engine/mcp/codex-config.mjs +12 -7
- package/driver/engine/mcp/dispositions-server.mjs +7 -0
- package/driver/engine/mcp/perplexity-server.mjs +44 -3
- package/driver/engine/mcp/recording-server.mjs +16 -1
- package/driver/engine/mcp/supplemental.mjs +6 -1
- package/driver/engine/openai-agent.mjs +62 -20
- package/driver/findings-model.mjs +31 -1
- package/driver/gateway.mjs +18 -2
- package/driver/owner-use-check.mjs +15 -6
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +91 -26
- package/driver/placement-decisions.mjs +20 -5
- package/driver/predelivery-lint.mjs +54 -12
- package/driver/publish/render-knockout.mjs +23 -4
- package/driver/record-replay.mjs +40 -0
- package/driver/register-digest-record.mjs +27 -8
- package/driver/register-plan.mjs +6 -2
- package/driver/repair-targets.mjs +81 -6
- package/driver/research-answers.mjs +149 -8
- package/driver/research-checks.mjs +84 -0
- package/driver/saved-findings-block.mjs +36 -0
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-search/phase2-execution.md +13 -8
- package/driver/skills/clearance-search/synthesis-rules.md +2 -2
- package/driver/skills/narrative-refutation/SKILL.md +1 -1
- package/driver/stage-context.mjs +31 -1
- package/driver/stages.mjs +42 -10
- package/driver/suite-census.json +100 -4
- package/driver/synthesis-record.mjs +278 -3
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/execute-plan.mjs +9 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/deprecate-below.mjs +14 -3
- package/scripts/e2e-first-time.mjs +2 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/shared/inside-run.mjs +54 -0
package/THIRD-PARTY-NOTICES.md
CHANGED
package/build-info.json
CHANGED
|
@@ -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`).
|
|
@@ -191,7 +191,7 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
|
|
|
191
191
|
| `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` \| `cloud` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback; **`cloud`** bills Claude per use through the reader's own cloud account, named by the Claude program's own switch — `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` or `CLAUDE_CODE_USE_BEDROCK`, or `ANTHROPIC_BASE_URL` alone for a gateway (see the credentials table below) — and deletes `ANTHROPIC_API_KEY` as subscription does. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`; `cloud` is refused. **Fail-loud, never a guess, never the subscription in its place:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws; on `anthropic-agent`, `cloud` with two switches on, or with none and no `ANTHROPIC_BASE_URL`, throws, and so does a switch left on under `subscription` or `api-key`, because the program bills that cloud whatever this says; a value that is none of the three throws on both engines. `clearotron start` and `clearotron doctor` report each refusal against the settings the services read, naming a value that is not a billing mode by its setting and never quoting it, and the runner refuses the search when it is ordered, before anything is spent. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`, and `cloud`, null when no cloud bills), and the attempt row also carries `providerReported`, the program's own word for who served the turn. |
|
|
192
192
|
| `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `clearotron doctor` refuses one. **Unset**, or set to the bare word `claude` / `codex` (which means the same), the engine uses the program on `PATH` when the machine has one, and otherwise the copy Clearotron installed in `~/.local/share/clearotron/engines`: `clearotron install` offers to put the chosen engine's program there (`@anthropic-ai/claude-code` or `@openai/codex`, this platform's build only), and `clearotron update` refreshes it. Nothing is bundled into the package. A value naming anything else is used as given and never falls back. `clearotron doctor` says which copy it found, and its version when npm installed that copy. |
|
|
193
193
|
| `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name; a decision of 2026-09-04 **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
|
|
194
|
-
| `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-
|
|
194
|
+
| `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-terra` / `gpt-6-luna` (from 2026-10-28; from 2026-10-09 the sweep tier shared `gpt-6.1-sol`, and before that they were `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`). Codex `0.159.0` or newer is required on a ChatGPT sign-in: older releases refuse `gpt-6.1-sol` with a 400. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
|
|
195
195
|
| ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
|
|
196
196
|
| `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
|
|
197
197
|
| `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox bypassed, for hosts where that sandbox cannot run (setup and `doctor --probe-engine` report it). It removes a defence: with it set, a stage's commands run with every permission of the account Clearotron runs as, not only in the run folder. Codex has no guard against a stage writing into the run's `_driver/` folder, with the sandbox or without it. Set it because the host forces it, never for convenience. |
|
|
@@ -161,7 +161,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
|
|
|
161
161
|
`CLEAROTRON_AI` (anthropic-agent | openai-agent; default anthropic-agent), `CLEAROTRON_AI_BILLING`
|
|
162
162
|
(subscription|api-key|cloud; cloud is Claude only), `CLEAROTRON_CODEX_PATH`,
|
|
163
163
|
`CLEAROTRON_OPENAI_AUTH_FILE`, `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` /
|
|
164
|
-
`CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-
|
|
164
|
+
`CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-terra / gpt-6-luna),
|
|
165
165
|
`CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
|
|
166
166
|
`CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
|
|
167
167
|
`CLEAROTRON_MAX_BUDGET_USD` (unset).
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.5.0-beta.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed: When a register search fails, the report's coverage still accounts for the unread searches beside it, instead of leaving them out.
|
|
8
|
+
- Fixed: When the search for an owner's use of a mark could not run, the report no longer says that search found nothing.
|
|
9
|
+
- Fixed: A register entry the engine placed on the watchlist now appears in the audit trail with its reason, instead of being left out.
|
|
10
|
+
- Fixed: A citation of a finding the review withdrew, in the report's summary panel or methodology, is now sent back for correction before delivery.
|
|
11
|
+
- Fixed: A clearance that searched common law or the United Kingdom is no longer flagged as claiming a search it did not run.
|
|
12
|
+
- New: Searches now use the latest models: Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5, and OpenAI's GPT-6.1 Sol, GPT-6.1 Terra and GPT-6 Luna.
|
|
13
|
+
- Fixed: When two parties use the same name without a registration, the report keeps a finding for each, instead of keeping only the last one saved.
|
|
14
|
+
|
|
15
|
+
## 0.5.0-beta.4
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Fixed: On the Codex engine, a clearance no longer sees temporary files that another clearance left on the same machine.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- Fixed: When the report's writing step is interrupted, the next attempt continues from the findings it had already finished instead of starting again.
|
|
23
|
+
- Fixed: On a crowded field, the report is written from the work the clearance has already done, without reading every register result again.
|
|
24
|
+
|
|
3
25
|
## 0.5.0-beta.3
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/driver/band-shape.mjs
CHANGED
|
@@ -1135,18 +1135,26 @@ export const AN_UNREAD_LISTS_FLOOR = "Place each floor here, or set it aside by
|
|
|
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 =
|
|
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)}.
|
|
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)}.
|
|
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 ?
|
|
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)); }
|
|
@@ -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 ?
|
|
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
|
}
|
|
@@ -171,7 +171,7 @@ export const E3_BACKLOG = [
|
|
|
171
171
|
{
|
|
172
172
|
stage: "common-law",
|
|
173
173
|
kind: "dictated-line-shape",
|
|
174
|
-
where: "driver/stages.mjs:
|
|
174
|
+
where: "driver/stages.mjs:1104 (emitted at 1891, 1920, 2100, 2320)",
|
|
175
175
|
surface: "stage-message",
|
|
176
176
|
evidence: "CROSS-CHECK HAND-OFF: … record it on its OWN line in EXACTLY the form \"CROSS-CHECK REQUIRED: <what> — <why>\" (that exact prefix; an em-dash between what and why; name the mark in CAPS in <what>). The driver parses ONLY this exact line shape…",
|
|
177
177
|
reparsedBy: "driver/doubt-ledger.mjs:183 CROSS_CHECK_RE = /^(?:[-*]\\s+)?CROSS-CHECK REQUIRED:\\s*(.+?)\\s+—\\s+(.+?)\\s*$/ → mintCrossCheckDoubts",
|
|
@@ -265,7 +265,7 @@ export const E3_BACKLOG = [
|
|
|
265
265
|
{
|
|
266
266
|
stage: "register-unit",
|
|
267
267
|
kind: "exactly-these-keys",
|
|
268
|
-
where: "driver/stages.mjs:
|
|
268
|
+
where: "driver/stages.mjs:2272",
|
|
269
269
|
surface: "stage-message",
|
|
270
270
|
evidence: "Every block you append MUST carry \"state\":\"enumerated\" (ONLY if you paged it to has_more:false) or \"state\":\"incomplete\" — EXACTLY those two strings; there is no \"verified\"/\"checked\"/\"complete\"/\"clean\" state, and any other value fails the stage.",
|
|
271
271
|
reparsedBy: "driver/named-band.mjs parseNamedBand (off-enum state fails validators.registerUnit)",
|
|
@@ -283,7 +283,7 @@ export const E3_BACKLOG = [
|
|
|
283
283
|
{
|
|
284
284
|
stage: "register-unit",
|
|
285
285
|
kind: "exactly-these-keys",
|
|
286
|
-
where: "driver/stages.mjs:
|
|
286
|
+
where: "driver/stages.mjs:4280 (the frame-reopen / scoped-retry message builder). A second number stood here and had been stale for some time: it pointed at a contract-element description rather than a builder, at its old line and at every mechanical shift of it. Two candidate builders sit beside 4164 and picking one would be a guess, so the wrong pointer is removed rather than moved a third time — one accurate citation beats one accurate and one invented.",
|
|
287
287
|
surface: "stage-message",
|
|
288
288
|
evidence: "Every block you append MUST carry \"state\":\"enumerated\" (ONLY if paged to has_more:false) or \"state\":\"incomplete\" — EXACTLY those two strings … (re-dispatch builders, which REPLACE def.message)",
|
|
289
289
|
reparsedBy: "driver/named-band.mjs parseNamedBand. Scope warning: these builders replace def.message on every escalation / envelope-close / frame-reopen dispatch, so an E3 lint that walks STAGES[*].message only never sees them",
|
|
@@ -524,7 +524,7 @@ export const E3_BACKLOG = [
|
|
|
524
524
|
kind: "literal-json-skeleton",
|
|
525
525
|
where: "driver/stages.mjs:3294",
|
|
526
526
|
surface: "stage-message",
|
|
527
|
-
evidence: "use_check = {\"source\",\"quality\"}: … quality: OPTIONAL, EXACTLY one of owner-site / independent / register-mirror … own_rights = {\"source\"}",
|
|
527
|
+
evidence: "use_check = {\"source\",\"quality\",\"question\"}: … quality: OPTIONAL, EXACTLY one of owner-site / independent / register-mirror … own_rights = {\"source\"}",
|
|
528
528
|
reparsedBy: "driver/verify.mjs checkFindingsSibling (finding_use_check_missing); driver/own-rights.mjs:19-22",
|
|
529
529
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
530
530
|
},
|
|
@@ -670,7 +670,7 @@ export const E3_BACKLOG = [
|
|
|
670
670
|
{
|
|
671
671
|
stage: "narrative-refutation",
|
|
672
672
|
kind: "dictated-line-shape",
|
|
673
|
-
where: "driver/stages.mjs:
|
|
673
|
+
where: "driver/stages.mjs:4835",
|
|
674
674
|
surface: "stage-message",
|
|
675
675
|
// RE-QUOTED BY CONVERSION 9. The LINE-TOKEN half is gone — no "anywhere on the line", no "[kind: …]
|
|
676
676
|
// token", no "a line with no token is treated as fact", because a kind is a typed field now and an
|
|
@@ -769,7 +769,7 @@ export const E3_BACKLOG = [
|
|
|
769
769
|
{
|
|
770
770
|
stage: "report-card",
|
|
771
771
|
kind: "literal-json-skeleton",
|
|
772
|
-
where: "driver/stages.mjs:
|
|
772
|
+
where: "driver/stages.mjs:3968",
|
|
773
773
|
surface: "stage-message",
|
|
774
774
|
evidence: "The finding's OWN record — the ONLY source for this card …:\\n```json\\n<JSON.stringify(finding, null, 2)>\\n```",
|
|
775
775
|
reparsedBy: "none — this is the INPUT side, and that is why it belongs in the survey: a full JSON object rendered into the prompt is exactly the mechanism #850 proves produced R-RECEIPT (the model pattern-matches a shown shape). E3's clause 1 as written (\"a code fence or inline example showing the exact object shape the model must emit\") does not reach an injected record, so the lint needs an explicit rule for shown-but-not-owed structure",
|
package/driver/coverage-form.mjs
CHANGED
|
@@ -373,7 +373,7 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
|
|
|
373
373
|
return r ? { narrowing: r.narrowing, ...(r.lists?.length ? { lists: r.lists } : {}), ...(r.not_covered?.length ? { not_covered: r.not_covered } : {}) } : {};
|
|
374
374
|
};
|
|
375
375
|
const entriesByQid = new Map((plan?.entries ?? []).map((e) => [e.qid, e]));
|
|
376
|
-
const open = openBlocksByAxis(skel, bandBlocksByAxis, plan);
|
|
376
|
+
const open = openBlocksByAxis(skel, bandBlocksByAxis, plan, { withDeferred: true });
|
|
377
377
|
// Axis order is the run's, not a set's iteration order: the plan-execution skeleton first (the order
|
|
378
378
|
// the plan dictated), then any active axis the skeleton does not name, then the canonical axis list as
|
|
379
379
|
// the final tie-break. Deterministic ⇒ the same run regenerates a byte-identical form every pass.
|
|
@@ -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} —
|
|
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: `
|
|
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} —
|
|
444
|
-
...(byteCapped ? { byte_cap_note: `${byteCapped} matched record(s) withheld to fit one tool response —
|
|
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} —
|
|
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} —
|
|
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} —
|
|
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
|
|
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
|
|
616
|
-
const LOOKUP_OFFSET_BAND = "offset (default 0: where in the matching records to start — a capped answer names the
|
|
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.
|
|
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,
|
|
@@ -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
|
|
279
|
-
//
|
|
280
|
-
// cannot get a command to print the account's files.
|
|
281
|
-
//
|
|
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
|
-
|
|
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"`, "");
|
|
@@ -60,6 +60,7 @@ import { MAX_ROWS_PER_CALL } from "../../disposition-call.mjs";
|
|
|
60
60
|
// The lane's second statement, and a different one: which coverage units were searched to what end. Its
|
|
61
61
|
// own module and its own file, never a row in the disposition form — see record_coverage_status below.
|
|
62
62
|
import { recordCoverageStatus, COMMON_LAW_COVERAGE_STATUSES } from "../../common-law-coverage-status.mjs";
|
|
63
|
+
import { insideRun } from "../../../shared/inside-run.mjs"; // a path a tool is handed is served only inside this run
|
|
63
64
|
|
|
64
65
|
// ── B — THE TYPED DISPOSITION TRANSPORT ─────────────────────────────────────────────────────────────
|
|
65
66
|
//
|
|
@@ -76,11 +77,17 @@ function specFrom(params) {
|
|
|
76
77
|
const { grid_spec_path } = params ?? {};
|
|
77
78
|
if (!grid_spec_path)
|
|
78
79
|
return { error: "ERROR: grid_spec_path is required — it is the same driver-written spec path the grid tool was given. Do not compose a path." };
|
|
80
|
+
// THIS RUN'S SPEC AND LEDGER ONLY (shared/inside-run.mjs): the run folder is the one the driver wired, never the call's.
|
|
81
|
+
const runDir = process.env.CLEAROTRON_BAND_RUN_DIR || "";
|
|
82
|
+
if (runDir && !insideRun(runDir, grid_spec_path))
|
|
83
|
+
return { error: "ERROR: grid_spec_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you." };
|
|
79
84
|
let spec;
|
|
80
85
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
81
86
|
catch (err) { return { error: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
|
|
82
87
|
if (!underStudioSegment(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
83
88
|
return { error: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
|
|
89
|
+
if (runDir && !insideRun(runDir, spec.output_path))
|
|
90
|
+
return { error: "ERROR: the grid spec's output_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you." };
|
|
84
91
|
return { spec };
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -29,7 +29,8 @@ import { parsePrRiskResults, connotationObligations, renderConnotationObligation
|
|
|
29
29
|
// What stays is the GRID path's own need — the obligations sidecar written beside the grid's ledger.
|
|
30
30
|
import { obligationsSidecarPath } from "../../disposition-tool.mjs";
|
|
31
31
|
import { writeGridProvenance } from "./grid-provenance.mjs"; // — what served the grid, beside the ledger
|
|
32
|
-
import {
|
|
32
|
+
import { insideRun } from "../../../shared/inside-run.mjs"; // a path a tool is handed is served only inside this run
|
|
33
|
+
import { keepResearchAnswer, isResearchAnswer, replayAnswersFrom, researchReplayOn, savedResearchAnswer } from "../../research-answers.mjs"; // each answer's text, kept for a later attempt of its stage; a replay answers from them
|
|
33
34
|
|
|
34
35
|
const API_KEY = process.env.PERPLEXITY_API_KEY || "";
|
|
35
36
|
|
|
@@ -219,14 +220,28 @@ function judgeBlock(cands, part, gridSpecPath, reserve = 0) {
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
async function research(params) {
|
|
222
|
-
|
|
223
|
+
// A REPLAY SENDS NOTHING OUT, so it is answered before the key is asked for: an offline replay needs none.
|
|
224
|
+
if (researchReplayOn(RUN_DIR)) return replayResearch(params);
|
|
225
|
+
// A REFUSED QUESTION IS LOGGED as a failed one is. It left no line, so the run's record could not tell a question
|
|
226
|
+
// refused for want of a credential from one never asked, and a writer whose every question was refused wrote
|
|
227
|
+
// "no result" with nothing in the log to say otherwise. The reason names the refusal, never the key.
|
|
228
|
+
if (!API_KEY) {
|
|
229
|
+
logCall("perplexity_research", { task: params?.task ?? "", ...(params?.grid_spec_path ? { grid: true } : {}) },
|
|
230
|
+
{ ok: false, error: "refused: no research credential" });
|
|
231
|
+
return "ERROR: PERPLEXITY_API_KEY not set — marketplace research unavailable.";
|
|
232
|
+
}
|
|
223
233
|
const { task, depth, model: modelOverride, allow_fetch = false, domain_filter, enable_sandbox = false, response_schema, schema_name, grid_spec_path, part } = params;
|
|
224
234
|
|
|
225
235
|
// ── Deterministic grid mode: the caller dictates the cells; the tool runs + captures them. ──
|
|
226
236
|
if (grid_spec_path) {
|
|
237
|
+
// THIS RUN'S SPEC, WRITING THIS RUN'S LEDGER: a spec, or a ledger it names, in another run's folder is never served.
|
|
238
|
+
if (RUN_DIR && !insideRun(RUN_DIR, grid_spec_path))
|
|
239
|
+
return "ERROR: grid_spec_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you.";
|
|
227
240
|
let spec;
|
|
228
241
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
229
242
|
catch (err) { return `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.`; }
|
|
243
|
+
if (RUN_DIR && !insideRun(RUN_DIR, spec.output_path))
|
|
244
|
+
return requiredLedgerRefusal("ERROR: the grid spec's output_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you.", { spec, gridSpecPath: grid_spec_path });
|
|
230
245
|
if (!underStudioSegment(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
231
246
|
return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
|
|
232
247
|
// — already recorded and complete? Answer from the ledger; do not buy the grid twice. This is also
|
|
@@ -297,7 +312,11 @@ async function research(params) {
|
|
|
297
312
|
const data = await callAgentAPI(API_KEY, body);
|
|
298
313
|
keepRaw("question", { task, preset }, data);
|
|
299
314
|
const text = enable_sandbox ? formatSandboxResponse(data, preset) : formatResponse(data, preset);
|
|
300
|
-
|
|
315
|
+
// A REPLY THE FORMATTER COULD NOT READ IS NO ANSWER, and is logged as a failure: it was logged as answered, beside
|
|
316
|
+
// completed searches, though it is not kept and is asked again (research-answers.mjs).
|
|
317
|
+
logCall("perplexity_research", asked, isResearchAnswer(text)
|
|
318
|
+
? { ok: true, bytes: String(text ?? "").length, citations: citationCount(data) }
|
|
319
|
+
: { ok: false, error: String(text ?? "").slice(0, 200) });
|
|
301
320
|
keepResearchAnswer(RUN_DIR, { session: SESSION, agent: AGENT, task, preset, text });
|
|
302
321
|
return text;
|
|
303
322
|
} catch (err) {
|
|
@@ -309,6 +328,28 @@ async function research(params) {
|
|
|
309
328
|
}
|
|
310
329
|
}
|
|
311
330
|
|
|
331
|
+
// ── A REPLAY: THE RUN'S OWN ANSWER, OR NOTHING ─────────────────────────────────────────────────────────
|
|
332
|
+
//
|
|
333
|
+
// In the experiment sandbox under `--research replay` (research-answers.mjs), a question the run already asked
|
|
334
|
+
// gets the answer the run got, and any other question, a grid among them, is refused: nothing goes out to the
|
|
335
|
+
// research service. Both are logged as a lookup is, marked as a replay, so the arm's reading log says which. The answers
|
|
336
|
+
// are read from the copy the marker names, outside the arm's folder, where the stage's own commands cannot reach them.
|
|
337
|
+
function replayResearch(params) {
|
|
338
|
+
const { task, grid_spec_path } = params ?? {};
|
|
339
|
+
if (grid_spec_path) {
|
|
340
|
+
logCall("perplexity_research", { task: task ?? "", grid: true, replay: true }, { ok: false, error: "replay: grid not replayed" });
|
|
341
|
+
return "ERROR: replay — this replays the run's saved research answers and runs no grid; nothing was sent to the research service.";
|
|
342
|
+
}
|
|
343
|
+
const from = replayAnswersFrom(RUN_DIR);
|
|
344
|
+
const saved = savedResearchAnswer(from, task);
|
|
345
|
+
if (!saved) {
|
|
346
|
+
logCall("perplexity_research", { task: task ?? "", replay: true }, { ok: false, error: from ? "replay: no saved answer" : "replay: the marker names no answers file" });
|
|
347
|
+
return "ERROR: replay — the run asked no question worded exactly like this one, so there is no saved answer to give; nothing was sent to the research service.";
|
|
348
|
+
}
|
|
349
|
+
logCall("perplexity_research", { task, replay: true }, { ok: true, bytes: saved.text.length, replayed: true });
|
|
350
|
+
return saved.text;
|
|
351
|
+
}
|
|
352
|
+
|
|
312
353
|
serve({
|
|
313
354
|
name: "perplexity", version: "0.1.0",
|
|
314
355
|
tools: [{
|
|
@@ -1046,7 +1046,10 @@ serve({
|
|
|
1046
1046
|
"are. The driver renders narrative.md and findings.json. Your coverage rows are joined to this " +
|
|
1047
1047
|
"run's plan-execution receipt as you send them, so a clean claim over a slice that did not run is " +
|
|
1048
1048
|
"refused here — in this turn, where restating it is free — rather than at a gate that can only " +
|
|
1049
|
-
"re-ask the whole stage. The answer tells you what was stored, or names the exact defect token."
|
|
1049
|
+
"re-ask the whole stage. The answer tells you what was stored, or names the exact defect token. " +
|
|
1050
|
+
"Save each finding as you complete it, alone or a few at a time, with `save_findings`: each is checked as " +
|
|
1051
|
+
"the record checks a finding and kept the moment it passes, so a stopped attempt loses none of them. The " +
|
|
1052
|
+
"call that sends the whole record takes every saved finding for a position it does not carry itself.",
|
|
1050
1053
|
inputSchema: {
|
|
1051
1054
|
type: "object",
|
|
1052
1055
|
// ── NO BLANKET `required`, AND THAT IS THE FIX FOR ─────────────────────────
|
|
@@ -1079,6 +1082,18 @@ serve({
|
|
|
1079
1082
|
// reads the run's own accepted-call record rather than trusting the shape. That check is older
|
|
1080
1083
|
// than this change and is the reason relaxing `required` costs nothing.
|
|
1081
1084
|
properties: {
|
|
1085
|
+
// A COMPLETED FINDING, KEPT BEFORE THE WHOLE RECORD EXISTS (synthesis-record.mjs, `saveFindings`). Its items
|
|
1086
|
+
// are findings as the record carries them, declared from the same key ladder as a patch row.
|
|
1087
|
+
save_findings: {
|
|
1088
|
+
type: "array",
|
|
1089
|
+
description:
|
|
1090
|
+
"COMPLETED FINDINGS, saved before the whole record exists. Send one or a few complete finding objects as the findings record carries them, and nothing else on the call. Each is checked as the record checks a finding and kept as soon as it passes; one that is refused leaves the others saved. A finding saved again for the same position replaces the earlier one. Saving writes no report: the record is complete only when you send `narrative` with `findings`, and that call takes every saved finding for a position it does not carry.",
|
|
1091
|
+
items: {
|
|
1092
|
+
type: "object",
|
|
1093
|
+
description: "One complete finding object, as the findings record carries it.",
|
|
1094
|
+
properties: Object.fromEntries(FINDING_KEYS_CURRENT.map((k) => [k, { description: `Part of the finding record; see findings-model.mjs for what \`${k}\` carries.` }])),
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1082
1097
|
findings_patch: {
|
|
1083
1098
|
type: "array",
|
|
1084
1099
|
description:
|
|
@@ -34,7 +34,8 @@ import { dirname, join } from "node:path";
|
|
|
34
34
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
35
35
|
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, planExactOrWidth, planPhoneticOrWidth, fingerprint, ownerIntersectionGap, goodsTextGap, resolveRegions, houseElementOf, withoutHouseElementTerms, containsFormSubstitution } from "../../register-plan.mjs";
|
|
36
36
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
37
|
-
import { OUTSTANDING_REASON } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
37
|
+
import { OUTSTANDING_REASON, runDriverDirOf } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
38
|
+
import { pathInRun } from "../../../shared/inside-run.mjs"; // the band path is served only inside this run
|
|
38
39
|
import { heldAnswers, questionKey } from "./held-answers.mjs";
|
|
39
40
|
import { bandWithSlicesOut, slicesTakenOutByAxis } from "../../../shared/slices-taken-out.mjs";
|
|
40
41
|
import { readWithheldFamilies } from "../../withheld-families.mjs";
|
|
@@ -391,6 +392,10 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
391
392
|
const proposals = Array.isArray(params?.proposals) ? params.proposals : null;
|
|
392
393
|
if (!axis || !outPath || !proposals || !proposals.length)
|
|
393
394
|
return { type: "text", text: "ERROR: axis, output_path and a non-empty proposals[] are all required." };
|
|
395
|
+
// The supplemental plan is written beside the band, so the band path must be this run's before anything is written.
|
|
396
|
+
const runOf = runDriverDirOf(tctx?.recordLog);
|
|
397
|
+
if (runOf && pathInRun(dirname(runOf), outPath) === null)
|
|
398
|
+
return { type: "text", text: "ERROR: output_path is outside this run's folder. Use the band path the driver gave you." };
|
|
394
399
|
|
|
395
400
|
const suppPath = join(dirname(outPath), `${axis}-supplemental-plan.json`);
|
|
396
401
|
// ── the supplemental plan INHERITS the frozen plan's regions (review findings 4/12) ───────────────
|