clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
package/driver/driver.config.mjs
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
-
// Central paths + tunables for the
|
|
3
|
+
// Central paths + tunables for the clearance-search deterministic driver.
|
|
4
4
|
//
|
|
5
5
|
// The driver runs as an ordinary UNIX service account (launched by systemd), NOT as an LLM agent.
|
|
6
6
|
// The agent exec-deny is a gateway agent-tool restriction; it does not apply to this OS process.
|
|
7
7
|
// Every value is env-overridable so the identical code runs from a developer's shell and from the
|
|
8
8
|
// systemd unit on a deployed host.
|
|
9
9
|
|
|
10
|
-
import { join, dirname, isAbsolute } from "node:path";
|
|
10
|
+
import { join, dirname, basename, isAbsolute, delimiter } from "node:path";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
-
import { readdirSync, existsSync, accessSync, statSync, statfsSync, constants as FS } from "node:fs";
|
|
12
|
+
import { readdirSync, existsSync, accessSync, statSync, statfsSync, readFileSync, realpathSync, openSync, readSync, closeSync, constants as FS } from "node:fs";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { isWsl } from "../shared/wsl.mjs"; // — the one answer to "is this Linux under Windows", which decides the /mnt/<drive> skip
|
|
14
15
|
import { envFrom } from "../shared/env-aliases.mjs"; // — an operator-facing name is the one an operator sets, and it has to work where they set it; — envFrom is the resolver that reads every spelling of it
|
|
15
16
|
import { invoke } from "../shared/invocation.mjs"; // — name a command the reader can actually type
|
|
16
17
|
import { envFileRead } from "../shared/env-local.mjs"; // — WHICH file to set it in, measured; null for a service that read none
|
|
17
|
-
import { numericSetting, resolveNumericSetting } from "./numeric-setting.mjs"; // — a number, or a refusal that names the variable; never NaN
|
|
18
|
+
import { numericSetting, resolveNumericSetting } from "./numeric-setting.mjs"; import { STUDIO_SEGMENTS, STUDIO_SEGMENT_RE, studioSegmentFor } from "../shared/pre-rename-spellings.mjs"; export { STUDIO_SEGMENTS, STUDIO_SEGMENT_RE, studioSegmentFor }; // the studio segment an install keeps its runs under // — a number, or a refusal that names the variable; never NaN
|
|
18
19
|
|
|
19
20
|
const { X_OK } = FS;
|
|
20
21
|
|
|
@@ -110,6 +111,23 @@ export const envGateOn = (name) => {
|
|
|
110
111
|
// so import-time captures silently pinned every test to the FIRST test's env (workspace root, pool,
|
|
111
112
|
// retries) — the root of the intermittent cross-test contamination flake. Getters make "set env, then
|
|
112
113
|
// run" mean what it says, in tests and in prod alike.
|
|
114
|
+
// The four skill folders the identifier rename moved. A store outside the product may still hold them,
|
|
115
|
+
// and a profile may still name them, under the old spelling.
|
|
116
|
+
const RENAMED_SKILL_DIRS = Object.freeze([
|
|
117
|
+
["clearance-search", "prelim-search"], ["clearance-register", "prelim-register"],
|
|
118
|
+
["clearance-common-law", "prelim-common-law"], ["clearance-variants", "prelim-variants"],
|
|
119
|
+
]);
|
|
120
|
+
/** `[rel, the same path under the folder's other spelling | null]`. PURE. */
|
|
121
|
+
export function skillSpellings(rel) {
|
|
122
|
+
const m = /^skills\/([a-z-]+)(\/.*)?$/.exec(String(rel ?? ""));
|
|
123
|
+
if (!m) return [rel, null];
|
|
124
|
+
for (const [now, before] of RENAMED_SKILL_DIRS) {
|
|
125
|
+
if (m[1] === now) return [rel, `skills/${before}${m[2] ?? ""}`];
|
|
126
|
+
if (m[1] === before) return [rel, `skills/${now}${m[2] ?? ""}`];
|
|
127
|
+
}
|
|
128
|
+
return [rel, null];
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
export const config = {
|
|
114
132
|
// / — A BLANK VALUE IS NOT A CONFIGURED VALUE. `process.env.X || default` treats " " as
|
|
115
133
|
// configured, because a whitespace-only string is TRUTHY in JavaScript — so a variable set to spaces
|
|
@@ -211,21 +229,8 @@ export const config = {
|
|
|
211
229
|
* still fails loudly against the canonical location rather than silently against the overlay.
|
|
212
230
|
*/
|
|
213
231
|
resolveSkillPath(relFromSkillsRoot) {
|
|
214
|
-
|
|
215
|
-
const overlay = this.skillsOverlayDir;
|
|
216
|
-
if (overlay) {
|
|
217
|
-
// FAIL LOUD ON AN UNREADABLE OVERLAY. existsSync() answers false for a permission error just as it
|
|
218
|
-
// does for a missing file, so a config store the process cannot read would silently resolve EVERY
|
|
219
|
-
// file to the repo — swapping a customer's own risk framework for the Generic default with nothing
|
|
220
|
-
// in the log to say so. A configured-but-unreadable overlay is a deploy defect, not a fallback.
|
|
221
|
-
if (!existsSync(overlay))
|
|
222
|
-
throw new Error(`skills_overlay_unreadable:${overlay} (CLEAROTRON_INSTRUCTIONS_DIR names it, set by the operator or derived by the portal from PROFILE_REPO_ROOT, but this process cannot see it — customer-specific skills would silently fall back to the repo defaults)`);
|
|
223
|
-
const p = join(dirname(overlay), rel);
|
|
224
|
-
if (existsSync(p)) return p;
|
|
225
|
-
}
|
|
226
|
-
return join(dirname(this.skillsBaseDir), rel);
|
|
232
|
+
return this.resolveSkillPathReport(relFromSkillsRoot).path;
|
|
227
233
|
},
|
|
228
|
-
|
|
229
234
|
/**
|
|
230
235
|
* WHICH LAYER ANSWERED, as a fact rather than a path — `resolveSkillPath` with its reasoning shown.
|
|
231
236
|
*
|
|
@@ -251,13 +256,24 @@ export const config = {
|
|
|
251
256
|
*/
|
|
252
257
|
resolveSkillPathReport(relFromSkillsRoot) {
|
|
253
258
|
const rel = String(relFromSkillsRoot ?? "").replace(/^\/+/, "");
|
|
254
|
-
|
|
259
|
+
// EITHER SPELLING OF A RENAMED SKILL FOLDER, in either direction. A store that has not moved its
|
|
260
|
+
// folders still holds `skills/prelim-search/…`, and a profile written before the rename still NAMES
|
|
261
|
+
// it; the product ships only the new names. Tried in the overlay first — a client's own doctrine must
|
|
262
|
+
// never be silently replaced by ours because a folder name changed — and the base answers only under
|
|
263
|
+
// the name it ships.
|
|
264
|
+
const [asNamed, other] = skillSpellings(rel);
|
|
265
|
+
const shipped = rel === asNamed && other && /^skills\/prelim-/.test(asNamed) ? other : asNamed;
|
|
266
|
+
const basePath = join(dirname(this.skillsBaseDir), shipped);
|
|
255
267
|
const overlay = this.skillsOverlayDir;
|
|
256
268
|
if (!overlay) return { path: basePath, rel, layer: existsSync(basePath) ? "base-only" : "missing", overlayPath: null, basePath };
|
|
257
269
|
if (!existsSync(overlay))
|
|
258
270
|
throw new Error(`skills_overlay_unreadable:${overlay} (CLEAROTRON_INSTRUCTIONS_DIR names it, set by the operator or derived by the portal from PROFILE_REPO_ROOT, but this process cannot see it — customer-specific skills would silently fall back to the repo defaults)`);
|
|
259
|
-
const overlayPath = join(dirname(overlay),
|
|
271
|
+
const overlayPath = join(dirname(overlay), asNamed);
|
|
260
272
|
if (existsSync(overlayPath)) return { path: overlayPath, rel, layer: "overlay", overlayPath, basePath };
|
|
273
|
+
if (other) {
|
|
274
|
+
const otherPath = join(dirname(overlay), other);
|
|
275
|
+
if (existsSync(otherPath)) return { path: otherPath, rel, layer: "overlay", overlayPath: otherPath, basePath };
|
|
276
|
+
}
|
|
261
277
|
return { path: basePath, rel, layer: existsSync(basePath) ? "base" : "missing", overlayPath, basePath };
|
|
262
278
|
},
|
|
263
279
|
|
|
@@ -275,7 +291,7 @@ export const config = {
|
|
|
275
291
|
return roots;
|
|
276
292
|
},
|
|
277
293
|
|
|
278
|
-
// The base that a profile's "skills/
|
|
294
|
+
// The base that a profile's "skills/clearance-search/<file>.md" path is relative to — i.e. the PARENT
|
|
279
295
|
// of skillsDir. Everything the DRIVER reads itself (framework manifests, band-meaning extraction)
|
|
280
296
|
// must join against this, exactly as the agent resolves the same relative paths against the
|
|
281
297
|
// skillsDir it is handed (gateway.mjs engineSkillsDir).
|
|
@@ -304,15 +320,17 @@ export const config = {
|
|
|
304
320
|
},
|
|
305
321
|
// Escaped prefix for the reverse regexes below (a custom prefix may carry regex metachars).
|
|
306
322
|
get workspacePrefixRe() { return this.workspacePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); },
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
323
|
+
// THE STUDIO SEGMENT IS NOT A PRODUCT NAME and does not follow the product rename. It is a directory
|
|
324
|
+
// segment on disk, and every archived run — its slug dirs, its `archive/`, its queue, its matter ledger —
|
|
325
|
+
// was written under it. Renaming it does not move those runs; it points the reader somewhere empty, and
|
|
326
|
+
// an empty directory reads as "no runs" rather than as an error. The identifier rename rewrote this
|
|
327
|
+
// comment together with the thirteen sites it protected, and an upgraded install lost sight of its whole
|
|
328
|
+
// archive. So an install keeps the segment it has: `prelim-search` wherever that directory exists, and
|
|
329
|
+
// `clearance-search` only for an install that has no other. Every site asks `studioSegmentFor`.
|
|
330
|
+
studioSegment(workspaceDir) { return studioSegmentFor(workspaceDir); },
|
|
314
331
|
studioRootForAgent(agentId) {
|
|
315
|
-
|
|
332
|
+
const ws = join(this.workspaceRoot, this.workspaceDirName(agentId));
|
|
333
|
+
return join(ws, "studio", studioSegmentFor(ws));
|
|
316
334
|
},
|
|
317
335
|
queueDirForAgent(agentId) {
|
|
318
336
|
return join(this.studioRootForAgent(agentId), "queue");
|
|
@@ -320,9 +338,10 @@ export const config = {
|
|
|
320
338
|
archiveRootForAgent(agentId) {
|
|
321
339
|
return join(this.studioRootForAgent(agentId), "archive");
|
|
322
340
|
},
|
|
323
|
-
// …/<prefix><id>/studio
|
|
341
|
+
// …/<prefix><id>/studio/<segment>/queue → "<id>"; null if the path isn't an agent queue dir. Either
|
|
342
|
+
// spelling of the segment, because an install keeps the one it has.
|
|
324
343
|
agentIdFromQueueDir(qdir) {
|
|
325
|
-
const m = new RegExp(`(?:^|/)${this.workspacePrefixRe}([^/]+)/studio/
|
|
344
|
+
const m = new RegExp(`(?:^|/)${this.workspacePrefixRe}([^/]+)/studio/${STUDIO_SEGMENT_RE}/queue/?$`).exec(qdir);
|
|
326
345
|
return m ? m[1] : null;
|
|
327
346
|
},
|
|
328
347
|
// Every agent workspace's clearotron queue. The systemd `.path` watches these and the runner drains ALL of
|
|
@@ -342,14 +361,18 @@ export const config = {
|
|
|
342
361
|
try {
|
|
343
362
|
for (const name of readdirSync(root)) {
|
|
344
363
|
if (this.agentIdFromWorkspaceName(name) == null) continue;
|
|
345
|
-
|
|
346
|
-
|
|
364
|
+
// BOTH spellings are drained where both exist: a job queued under either is a job somebody is
|
|
365
|
+
// waiting on, and an unwatched queue looks exactly like an empty one.
|
|
366
|
+
for (const seg of STUDIO_SEGMENTS) {
|
|
367
|
+
const q = join(root, name, "studio", seg, "queue");
|
|
368
|
+
if (existsSync(q)) dirs.push(q);
|
|
369
|
+
}
|
|
347
370
|
}
|
|
348
371
|
} catch { /* workspaceRoot may not exist in some test envs — fall through to the canonical queue */ }
|
|
349
372
|
// THE ONE AGENT NAME NO CONFIGURATION REMOVES, and it is deliberate. row 5.
|
|
350
373
|
//
|
|
351
374
|
// `this.queueDir` is `queueDirForAgent("clawdi")` — a LITERAL, not `defaultAgent` — so every
|
|
352
|
-
// deployment, however configured, watches `<workspacePrefix>clawdi/studio/
|
|
375
|
+
// deployment, however configured, watches `<workspacePrefix>clawdi/studio/clearance-search/queue`.
|
|
353
376
|
// Setting CLEAROTRON_DEFAULT_AGENT does not remove it (prod runs `ops`, dev runs `dev`, and both still
|
|
354
377
|
// watch this); nor does CLEAROTRON_WORKSPACE_PREFIX. An installer who copies `.env.example` gets a
|
|
355
378
|
// neutral default agent AND this directory.
|
|
@@ -539,7 +562,7 @@ export const config = {
|
|
|
539
562
|
// directory, so renaming the default moves the install to an empty one and nothing migrates. Here
|
|
540
563
|
// the orphaned files are run-slot locks, so a live run's slot goes unseen and the global cap is
|
|
541
564
|
// silently exceeded rather than enforced. Ruling.
|
|
542
|
-
get runLockDir() { return this.envValue("CLEAROTRON_RUN_LOCK_DIR") || join(this.workspaceRoot, "prelim-run-locks"); },
|
|
565
|
+
get runLockDir() { return this.envValue("CLEAROTRON_RUN_LOCK_DIR") || [join(this.workspaceRoot, "prelim-run-locks"), join(this.workspaceRoot, "clearance-run-locks")].find((d, i) => i === 1 || existsSync(d)); }, // the name the install already has wins
|
|
543
566
|
|
|
544
567
|
// Delivery outbox (Workstream B). On a handoff-mode finish the driver drops <runId>.pending here (naming
|
|
545
568
|
// the forwarder agent); the systemd-user prelim-outbox.path unit fires an INSTANT clearotron-deliver wake off
|
|
@@ -551,7 +574,31 @@ export const config = {
|
|
|
551
574
|
// directory, so renaming the default moves the install to an empty one and nothing migrates. Here
|
|
552
575
|
// the orphaned files are requester-facing events — delivered, run-failed, intake-rejected — so the
|
|
553
576
|
// visible failure is a requester never told their run finished. Ruling.
|
|
554
|
-
get outboxDir() { return this.envValue("CLEAROTRON_OUTBOX_DIR") || join(this.workspaceRoot, "
|
|
577
|
+
get outboxDir() { return this.envValue("CLEAROTRON_OUTBOX_DIR") || join(this.workspaceRoot, "clearance-outbox"); },
|
|
578
|
+
|
|
579
|
+
// ── THE DIRECTORY THIS DEFAULT USED TO NAME, AND WHY IT IS STILL READ ──────────────────────────────
|
|
580
|
+
//
|
|
581
|
+
// This default was `clearance-outbox` until the identifier was renamed. A deployment that never pinned
|
|
582
|
+
// `CLEAROTRON_OUTBOX_DIR` has its `<runId>.pending` markers sitting in the old directory, and moving
|
|
583
|
+
// the default without reading the old one orphans every one of them. That failure is silent and it is
|
|
584
|
+
// the expensive kind: a marker is a report a client is OWED, an unread outbox is indistinguishable
|
|
585
|
+
// from nothing to send, and the run that produced it has already recorded itself as delivered-pending.
|
|
586
|
+
//
|
|
587
|
+
// NULL WHEN THE VARIABLE IS SET, because then the operator named the directory and there is no old
|
|
588
|
+
// default in play. Writers use `outboxDir` alone; only readers consult this, which is the same posture
|
|
589
|
+
// the run records take — new work uses the new name, old work is still understood.
|
|
590
|
+
//
|
|
591
|
+
// THE OLD SPELLING IS THE WHOLE POINT OF THIS ACCESSOR, and a sweep took it. The identifier rename
|
|
592
|
+
// moved the directory from `prelim-outbox` to `clearance-outbox`, and the follow-up that renamed the
|
|
593
|
+
// directory's 85 occurrences rewrote this literal along with them — leaving the accessor that exists to
|
|
594
|
+
// name the OLD directory naming the new one. Both getters then answered the same path: the drain read
|
|
595
|
+
// the new directory twice, never read the old one, and every marker under it was listed TWICE. So a box
|
|
596
|
+
// that never pinned the variable got exactly the failure the block above describes — orphaned markers,
|
|
597
|
+
// silently — plus duplicate work on the ones it could see. It is written once, here, and pinned by a
|
|
598
|
+
// test that asserts the two are different.
|
|
599
|
+
get legacyOutboxDir() {
|
|
600
|
+
return this.envValue("CLEAROTRON_OUTBOX_DIR") ? null : join(this.workspaceRoot, "prelim-outbox");
|
|
601
|
+
},
|
|
555
602
|
|
|
556
603
|
// ── Delivery/comms (Phase 2, standalone product) ─────────────────────────────────────────────────
|
|
557
604
|
// THERE IS ONE MODE AND IT IS NOT A SETTING. The driver SENDS NOTHING: every requester-facing event
|
|
@@ -569,7 +616,7 @@ export const config = {
|
|
|
569
616
|
// LOCATION (see studioRootForAgent / agentIdFromQueueDir above).
|
|
570
617
|
//
|
|
571
618
|
// THE DEFAULT IS PART OF A PATH, so changing it moves where an install looks for its own runs:
|
|
572
|
-
// every run dir is `<workspaceRoot>/workspace-<agent>/studio/
|
|
619
|
+
// every run dir is `<workspaceRoot>/workspace-<agent>/studio/clearance-search/…`. An install created
|
|
573
620
|
// before this default changed keeps its runs under the old id and must pin it — both spellings,
|
|
574
621
|
// because the gather servers read their own variable:
|
|
575
622
|
//
|
|
@@ -616,12 +663,12 @@ export const MODELS = {
|
|
|
616
663
|
gemini: "google/gemini-3.1-pro-preview",
|
|
617
664
|
"gemini-flash": "google/gemini-3-flash-preview",
|
|
618
665
|
"deepseek-v4-pro": "together/deepseek-ai/DeepSeek-V4-Pro",
|
|
619
|
-
// azure =
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
azure:
|
|
666
|
+
// azure = a legacy catalogue entry for an Azure GPT deployment. NO STAGE NAMES IT AND NO ENGINE RUNS IT:
|
|
667
|
+
// the Claude adapter refuses the alias and the codex adapter maps no tier onto it. So its target is a
|
|
668
|
+
// constant. The setting that used to override it was retired on 2026-09-15 and nothing reads it any
|
|
669
|
+
// more; the configuration reference lists it under the settings that do nothing. It is named only
|
|
670
|
+
// there, in prose, because a name written in a source file here counts as a name this build reads.
|
|
671
|
+
azure: "azure-openai/gpt-5.4",
|
|
625
672
|
};
|
|
626
673
|
|
|
627
674
|
// alias → full id; a value that's already a full provider/model id (contains "/") passes through.
|
|
@@ -659,6 +706,13 @@ export function resolveModel(model) {
|
|
|
659
706
|
* never a default. A null on either side makes the comparison UNKNOWN, and an unknown must never be
|
|
660
707
|
* recorded as a match — that is the absence-read-as-a-pass class this whole issue is about.
|
|
661
708
|
*/
|
|
709
|
+
// FABLE IS NOT A FAMILY HERE, ON PURPOSE. Nobody has seen what the wire reports for a fable turn, so an id
|
|
710
|
+
// naming fable stays unknown and its comparison can never manufacture a mismatch. Placing it was tried
|
|
711
|
+
// (2026-09-15) and refused turns that had always run: a fable request served by claude-sonnet-5, by
|
|
712
|
+
// claude-opus-5, by a deployment named after another tier or by a GPT id, an opus request served as
|
|
713
|
+
// claude-fable-5-1, and any tier served under a name beginning with the word. It was taken out again the
|
|
714
|
+
// same day. The report's model line still names Fable: servedModels (tokens.mjs) reads a fable request's tier
|
|
715
|
+
// itself, where only the report sees it, and never through this comparison.
|
|
662
716
|
const MODEL_FAMILY_RE = /(?:^|\/)(?:claude-)?(opus|sonnet|haiku)(?:[-.]|$)/i;
|
|
663
717
|
|
|
664
718
|
// — THE OPENAI SIDE, added when the codex path could first answer "what ran".
|
|
@@ -931,7 +985,7 @@ export const PROVIDERS = {
|
|
|
931
985
|
id: "corsearch",
|
|
932
986
|
label: "Corsearch",
|
|
933
987
|
credEnv: "CORSEARCH_SESSION_KEY",
|
|
934
|
-
skillDoc: "skills/
|
|
988
|
+
skillDoc: "skills/clearance-register/providers/corsearch.md",
|
|
935
989
|
hasPublicRecordUrl: true,
|
|
936
990
|
// WP-receipts W2: the public per-record origin (publicRecordOrigin + /mark/<jur>/<id> is a working
|
|
937
991
|
// link) — replaces the fragile resolved-link-origin inference at render for receipt-carrying runs.
|
|
@@ -1005,7 +1059,7 @@ export const PROVIDERS = {
|
|
|
1005
1059
|
id: "clarivate",
|
|
1006
1060
|
label: "Clarivate Compumark",
|
|
1007
1061
|
credEnv: "CLARIVATE_API_KEY",
|
|
1008
|
-
skillDoc: "skills/
|
|
1062
|
+
skillDoc: "skills/clearance-register/providers/clarivate.md",
|
|
1009
1063
|
hasPublicRecordUrl: false, // Compumark Content has no public record URL — cite the office register
|
|
1010
1064
|
//, ruling 2026-08-20 — WHAT A CARD SHOWS WHERE A LINK CANNOT GO. A UI exists for this
|
|
1011
1065
|
// provider and we do not know its per-record URL, so the card says so and says it is unfinished.
|
|
@@ -1147,7 +1201,7 @@ export const PROVIDERS = {
|
|
|
1147
1201
|
id: "signa",
|
|
1148
1202
|
label: "Signa",
|
|
1149
1203
|
credEnv: "SIGNA_API_KEY",
|
|
1150
|
-
skillDoc: "skills/
|
|
1204
|
+
skillDoc: "skills/clearance-register/providers/signa.md",
|
|
1151
1205
|
hasPublicRecordUrl: false, // Signa exposes no per-record public URL — cite the office register
|
|
1152
1206
|
//, ruling 2026-08-20 — no register UI exists to link to at all, so the card points at
|
|
1153
1207
|
// the artifact that DOES carry the record: the audit workbook. Naming it is the whole of this
|
|
@@ -1202,8 +1256,12 @@ export const PROVIDERS = {
|
|
|
1202
1256
|
// on the search response, so this no longer has to answer `present` and nothing else. It is
|
|
1203
1257
|
// still null whenever the vendor would only approximate it — and null there means UNKNOWN,
|
|
1204
1258
|
// which is the whole reason the field may never be filled in with a figure from anywhere else.
|
|
1259
|
+
// `floor` rides beside `approximate` because the two are one fact: the register answered, and the
|
|
1260
|
+
// answer is "more than this". Without the number the disclosure is not usable — "approximate"
|
|
1261
|
+
// alone says no more than "unknown" does, which is the state this replaces.
|
|
1205
1262
|
return { ok: true, total: Number.isFinite(p.total_hits) ? p.total_hits : null,
|
|
1206
|
-
approximate: p.total_approximate === true,
|
|
1263
|
+
approximate: p.total_approximate === true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null,
|
|
1264
|
+
present: p.present === true, note: p.note };
|
|
1207
1265
|
} catch (e) { return { ok: false, cause: `countHits threw: ${e.message}` }; }
|
|
1208
1266
|
},
|
|
1209
1267
|
// `reason`, not `cause`, on every refusal: the listing reads `reason` (register-records.mjs), so a
|
|
@@ -1293,7 +1351,7 @@ export const PROVIDERS = {
|
|
|
1293
1351
|
// list — without this, an instance holding the id and no secret passes preflight and dies on the
|
|
1294
1352
|
// first token request, after model spend and reported as a provider fault.
|
|
1295
1353
|
credEnvAlso: ["EUIPO_CLIENT_SECRET"],
|
|
1296
|
-
skillDoc: "skills/
|
|
1354
|
+
skillDoc: "skills/clearance-register/providers/euipo.md",
|
|
1297
1355
|
hasPublicRecordUrl: true,
|
|
1298
1356
|
publicRecordOrigin: "https://euipo.europa.eu",
|
|
1299
1357
|
async recordFetch(uri, { agentId, sessionKey, recordLog = null }) {
|
|
@@ -1359,7 +1417,7 @@ export const PROVIDERS = {
|
|
|
1359
1417
|
id: "uspto-local",
|
|
1360
1418
|
label: "USPTO (local index)",
|
|
1361
1419
|
credEnv: "USPTO_LOCAL_DB",
|
|
1362
|
-
skillDoc: "skills/
|
|
1420
|
+
skillDoc: "skills/clearance-register/providers/uspto-local.md",
|
|
1363
1421
|
hasPublicRecordUrl: true,
|
|
1364
1422
|
// TSDR publishes a page per serial, so a finding can cite an address the reader can open. The
|
|
1365
1423
|
// record ref is /mark/us/<serial>, and the core builds the full statusSearch link on the record.
|
|
@@ -1463,7 +1521,7 @@ export const PROVIDERS = {
|
|
|
1463
1521
|
label: "Free tier (EUIPO + USPTO local index)",
|
|
1464
1522
|
credEnv: "EUIPO_CLIENT_ID",
|
|
1465
1523
|
credEnvAlso: ["EUIPO_CLIENT_SECRET"],
|
|
1466
|
-
skillDoc: "skills/
|
|
1524
|
+
skillDoc: "skills/clearance-register/providers/free-tier.md",
|
|
1467
1525
|
hasPublicRecordUrl: true,
|
|
1468
1526
|
// NULL, deliberately: the two members have DIFFERENT public origins (euipo.europa.eu and the USPTO),
|
|
1469
1527
|
// so a single origin string here would stamp one office's host onto the other's citations. The
|
|
@@ -1813,7 +1871,7 @@ export function preflightCredentials(env = process.env) {
|
|
|
1813
1871
|
// paid for. This door is that same failure, moved in front of the spend.
|
|
1814
1872
|
//
|
|
1815
1873
|
// GATED ON THE COMPONENT, NEVER ON THE PIPELINE. `pipeline === "clearance"` is the wrong predicate and
|
|
1816
|
-
// fails in the expensive direction: `
|
|
1874
|
+
// fails in the expensive direction: `clearance-register-only` is a clearance that carries
|
|
1817
1875
|
// `commonLawGrid: false`, searches no unregistered-use half by design, and would be refused for a
|
|
1818
1876
|
// credential its lane never reads. The component IS the question — search-policy.mjs calls it "the
|
|
1819
1877
|
// clearance's unregistered-use half" in as many words.
|
|
@@ -1910,26 +1968,32 @@ export const ENGINE_BINARIES = {
|
|
|
1910
1968
|
// `vendor` is the one word a person needs — the staff config page answers "which engine is
|
|
1911
1969
|
// running the searches", and `label` below is the MECHANISM, which is what took off that page.
|
|
1912
1970
|
vendor: "Anthropic",
|
|
1971
|
+
// `product` is the name a reader knows the program by, beside the vendor in setup's engine question
|
|
1972
|
+
// ("Claude, by Anthropic"). Not `fallback`, which is the command word and lower-case.
|
|
1973
|
+
product: "Claude",
|
|
1913
1974
|
env: "CLEAROTRON_CLAUDE_PATH", fallback: "claude",
|
|
1975
|
+
// The npm package that carries this program, and the oldest version setup installs: "this version or
|
|
1976
|
+
// newer", with no ceiling. Setup installs it into the engines folder (enginesFolder, below the table)
|
|
1977
|
+
// when the reader picks this engine, and the resolver uses it only when the machine has no copy of its
|
|
1978
|
+
// own. The package's own `bin` field names the program, so no path inside it is written down here.
|
|
1979
|
+
package: "@anthropic-ai/claude-code", floor: "2.1.270",
|
|
1980
|
+
// WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
|
|
1981
|
+
// declared by the vendor: the engines folder after a fresh install of this package into an empty
|
|
1982
|
+
// folder, on npm 10.9.8 and on 11.19.1, 2026-09-14. A later release can be larger or smaller, so setup
|
|
1983
|
+
// says "about". Re-measure when the floor moves.
|
|
1984
|
+
installMB: 214,
|
|
1985
|
+
// The licence setup states wherever it tells a reader what they are about to install or use, as the
|
|
1986
|
+
// vendor's package declares it: "SEE LICENSE IN README.md", Anthropic's own terms.
|
|
1987
|
+
licence: "proprietary third-party software",
|
|
1914
1988
|
label: "Anthropic — each stage runs as a headless `claude -p` turn",
|
|
1915
1989
|
module: "engine/anthropic-agent.mjs", adapter: "anthropicAgentEngine",
|
|
1916
1990
|
signIn: "run `claude` once in a terminal and complete the sign-in",
|
|
1917
1991
|
authEnv: "CLEAROTRON_AI_BILLING", apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
1918
1992
|
subscriptionHow: "sign in once with `claude`",
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
// npm, NOT the vendor's shell installer. `curl … | bash` is the other documented route for this CLI
|
|
1924
|
-
// and the wizard will not run one: a command this product executes on someone's box has to be one
|
|
1925
|
-
// they can read in full before they answer, and a piped remote script is not.
|
|
1926
|
-
install: "npm install -g @anthropic-ai/claude-code",
|
|
1927
|
-
// — THE NO-ROOT ROUTE, NAMED AND NEVER EXECUTED. The stance above holds: this
|
|
1928
|
-
// product does not run a piped remote script. But on a box whose npm prefix needs root, the npm
|
|
1929
|
-
// route CANNOT work as this user, and offering only it was a dead end the owner hit. The wizard
|
|
1930
|
-
// prints this for the reader to run BY THEIR OWN HAND in another terminal — their shell, their
|
|
1931
|
-
// eyes, their decision — and says where it lands so the path answer afterwards is not a guess.
|
|
1932
|
-
installNoRoot: { cmd: "curl -fsSL https://claude.ai/install.sh | bash", lands: "~/.local/bin" },
|
|
1993
|
+
// `install`, the command that puts this program in the engines folder, is written in below the table
|
|
1994
|
+
// from `package` and `floor`, so the command a reader is shown and the one setup runs cannot drift.
|
|
1995
|
+
// npm, NOT the vendor's shell installer: a command this product executes on someone's box has to be
|
|
1996
|
+
// one they can read in full before they answer, and a piped remote script is not.
|
|
1933
1997
|
// — the documented headless ending. `claude setup-token` walks the sign-in and
|
|
1934
1998
|
// prints a long-lived token; the stage subprocess env is a spread of the driver's — spawnEnv
|
|
1935
1999
|
// strips ONLY the API key under subscription — so a token in the env file reaches the CLI
|
|
@@ -1940,23 +2004,66 @@ export const ENGINE_BINARIES = {
|
|
|
1940
2004
|
},
|
|
1941
2005
|
"openai-agent": {
|
|
1942
2006
|
vendor: "OpenAI",
|
|
2007
|
+
product: "Codex",
|
|
1943
2008
|
env: "CLEAROTRON_CODEX_PATH", fallback: "codex",
|
|
2009
|
+
package: "@openai/codex", floor: "0.154.0",
|
|
2010
|
+
installMB: 324, // measured the same way and on the same day as Claude's, above
|
|
2011
|
+
licence: "third-party software under the Apache-2.0 licence", // the package's own "license" field
|
|
1944
2012
|
label: "OpenAI — each stage runs as a headless `codex exec` turn",
|
|
1945
2013
|
module: "engine/openai-agent.mjs", adapter: "openaiAgentEngine",
|
|
1946
2014
|
signIn: "run `codex login`",
|
|
1947
2015
|
authEnv: "CLEAROTRON_AI_BILLING", apiKeyEnv: "CODEX_API_KEY",
|
|
1948
2016
|
subscriptionHow: "sign in once with `codex login` — the adapter reads ~/.codex/auth.json and refuses before spending if it is absent",
|
|
1949
|
-
// Verified against the registry 2026-08-24: 0.149.1.
|
|
1950
|
-
install: "npm install -g @openai/codex",
|
|
1951
|
-
// — no vendor shell installer exists for this CLI; the no-root answer on a
|
|
1952
|
-
// root-only prefix is npm's own prefix move, which the wizard names the same way.
|
|
1953
|
-
installNoRoot: null,
|
|
1954
2017
|
// — codex's headless ending writes its own ~/.codex/auth.json; there is no
|
|
1955
2018
|
// token to capture into an env file, and inventing one would be a route nobody has driven.
|
|
1956
2019
|
headless: { cmd: "codex login --device-auth", tokenEnv: null },
|
|
1957
2020
|
},
|
|
1958
2021
|
};
|
|
1959
2022
|
|
|
2023
|
+
// ── WHERE SETUP INSTALLS AN ENGINE'S PROGRAM, AND THE COMMAND THAT DOES IT ─────────────────────────────
|
|
2024
|
+
//
|
|
2025
|
+
// Nothing is bundled into the package. Setup installs the ONE program the reader's engine runs, when they
|
|
2026
|
+
// say yes, as an ordinary npm project in a folder Clearotron owns under their home directory: npm then
|
|
2027
|
+
// fetches that platform's binary and nothing else, and needs no root. The folder is not on PATH, so a copy
|
|
2028
|
+
// the machine installs itself is found first (resolveEngineProgram). `clearotron update` runs the same
|
|
2029
|
+
// install again, which moves the program to the newest its vendor publishes: measured on npm 10.9.8 and
|
|
2030
|
+
// 11.19.1, re-running an install with a `>=` range over an older copy moves it to the newest, where
|
|
2031
|
+
// `npm update` would stop at the caret npm writes into the folder's package.json.
|
|
2032
|
+
|
|
2033
|
+
/** Where to install, and look for, the engine programs instead of the default folder. An EMPTY directory means none. */
|
|
2034
|
+
export const ENGINES_DIR_ENV = "CLEAROTRON_ENGINES_DIR";
|
|
2035
|
+
|
|
2036
|
+
/** The default engines folder as a reader types it. */
|
|
2037
|
+
const ENGINES_FOLDER_TYPED = "~/.local/share/clearotron/engines";
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* The folder setup installs engine programs into and the resolver's last step reads: ENGINES_DIR_ENV from
|
|
2041
|
+
* this process when set, otherwise `~/.local/share/clearotron/engines`. Under the home directory rather
|
|
2042
|
+
* than the install's own tree, because an update replaces that tree, and because every service runs as a
|
|
2043
|
+
* user unit under the same home as the setup that installed it. XDG_DATA_HOME is not consulted: a shell's
|
|
2044
|
+
* value does not reach the services, and the two would then look in different folders.
|
|
2045
|
+
*/
|
|
2046
|
+
export function enginesFolder({ env = process.env, home = homedir() } = {}) {
|
|
2047
|
+
return String(env[ENGINES_DIR_ENV] ?? "").trim() || join(home, ".local", "share", "clearotron", "engines");
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/** The npm arguments that install, or refresh, an engine's program in `dir`: "this version or newer". */
|
|
2051
|
+
export function engineInstallArgs(spec, dir = enginesFolder()) {
|
|
2052
|
+
return ["install", "--prefix", dir, "--no-fund", "--no-audit", `${spec.package}@>=${spec.floor}`];
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/** A shell word as a reader pastes it: quoted when it must be, because a bare `>=` is a redirection. */
|
|
2056
|
+
const shellWord = (w) => (/^[\w@%+=:,./~-]+$/.test(w) ? w : `'${String(w).replace(/'/g, "'\\''")}'`);
|
|
2057
|
+
|
|
2058
|
+
/** The same install as a command a reader can paste. The default folder is written the way they type it. */
|
|
2059
|
+
export function engineInstallCommand(spec, dir = ENGINES_FOLDER_TYPED) {
|
|
2060
|
+
return ["npm", ...engineInstallArgs(spec, dir)].map(shellWord).join(" ");
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// The printable command each engine carries, for the readers that show one without a folder to hand (the
|
|
2064
|
+
// portal's engine page, doctor's way out of demo mode), made from the same parts as the install setup runs.
|
|
2065
|
+
for (const spec of Object.values(ENGINE_BINARIES)) spec.install = engineInstallCommand(spec);
|
|
2066
|
+
|
|
1960
2067
|
/** The production default, in ONE place rather than a literal repeated at every reader. */
|
|
1961
2068
|
export const DEFAULT_ENGINE_ID = "anthropic-agent";
|
|
1962
2069
|
|
|
@@ -1971,48 +2078,190 @@ export function engineAdapterSpecifier(engine) {
|
|
|
1971
2078
|
return spec ? pathToFileURL(join(DRIVER_DIR, spec.module)).href : null;
|
|
1972
2079
|
}
|
|
1973
2080
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2081
|
+
// ── WHERE AN ENGINE'S PROGRAM IS FOUND: ONE PLACE, AND EVERY READER ASKS IT ─────────────────────────────
|
|
2082
|
+
//
|
|
2083
|
+
// The run door, the inventory the portal reads, the wizard, doctor and both adapters all ask this one
|
|
2084
|
+
// function. Before it there were four answers: this file's PATH walk, the wizard's own walk (the only one
|
|
2085
|
+
// that passed over a Windows copy under WSL), and each adapter handing spawn(2) a bare word so the OS made
|
|
2086
|
+
// its own choice. They agreed only while every copy lived on PATH.
|
|
2087
|
+
//
|
|
2088
|
+
// THE ORDER, and why the machine's own copy wins:
|
|
2089
|
+
// 1. The explicit setting (`CLEAROTRON_CLAUDE_PATH` / `CLEAROTRON_CODEX_PATH`). A value that is set and
|
|
2090
|
+
// unusable is REPORTED, never overruled: the reader stated it, and quietly resolving somewhere else
|
|
2091
|
+
// would run a program nobody chose. The engine's own fallback word (`claude`, `codex`) is the default
|
|
2092
|
+
// spelled out, which is how the shipped example files write it, so it means exactly what unset means.
|
|
2093
|
+
// 2. The program on PATH: the machine's own install, which keeps updating itself.
|
|
2094
|
+
// 3. The copy setup installed in the engines folder (enginesFolder, above), only when the machine has
|
|
2095
|
+
// none. That folder is not on PATH. If a reader puts its node_modules/.bin there, a PATH hit that IS
|
|
2096
|
+
// that copy is passed over in step 2 and taken for what it is in step 3, so it is never reported, or
|
|
2097
|
+
// written into a settings file, as the machine's own.
|
|
2098
|
+
//
|
|
2099
|
+
// FILESYSTEM ONLY, like the rest of this door (see the header above ENGINE_BINARIES): nothing is spawned.
|
|
2100
|
+
|
|
2101
|
+
/** A path on a Windows drive as WSL mounts it. */
|
|
2102
|
+
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
2103
|
+
|
|
2104
|
+
const isExecFile = (p) => { try { return statSync(p).isFile() && (accessSync(p, X_OK), true); } catch { return false; } };
|
|
2105
|
+
const realOrNull = (p) => { try { return realpathSync(p); } catch { return null; } };
|
|
2106
|
+
const readPackage = (dir) => { try { return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")); } catch { return null; } };
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* Whether the kernel runs this file ITSELF: a native executable (ELF, Mach-O, a Windows PE) or a `#!` script.
|
|
2110
|
+
*
|
|
2111
|
+
* Asked only of a file inside the vendor's own package, for one measured reason. The Claude package ships
|
|
2112
|
+
* `bin/claude.exe` as a 500-byte shell placeholder and puts the real program over it in its install step.
|
|
2113
|
+
* With `--ignore-scripts`, or with the platform's native package missing, the placeholder stays. It is a
|
|
2114
|
+
* regular file with the execute bit, so the executable check accepts it, and spawn runs it through `sh`:
|
|
2115
|
+
* it prints "claude native binary not installed" and exits 1, at every stage. It has neither `#!` nor a
|
|
2116
|
+
* binary header, so this refuses it at the door instead. A file anywhere else is judged as before; a
|
|
2117
|
+
* reader's own wrapper script is not ours to second-guess.
|
|
2118
|
+
*/
|
|
2119
|
+
function runsDirectly(file) {
|
|
2120
|
+
const head = Buffer.alloc(4);
|
|
2121
|
+
let fd = null, n = 0;
|
|
2122
|
+
try { fd = openSync(file, "r"); n = readSync(fd, head, 0, 4, 0); }
|
|
2123
|
+
catch { return false; }
|
|
2124
|
+
finally { if (fd !== null) { try { closeSync(fd); } catch { /* nothing left to release */ } } }
|
|
2125
|
+
if (n >= 2 && head[0] === 0x23 && head[1] === 0x21) return true; // #!
|
|
2126
|
+
if (n >= 2 && head[0] === 0x4d && head[1] === 0x5a) return true; // MZ
|
|
2127
|
+
if (n < 4) return false;
|
|
2128
|
+
const word = head.readUInt32BE(0);
|
|
2129
|
+
return word === 0x7f454c46 // ELF
|
|
2130
|
+
|| [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe].includes(word); // Mach-O, thin and universal
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/** The npm package a file belongs to: the nearest package.json within a few levels of its real path. */
|
|
2134
|
+
function owningPackage(file) {
|
|
2135
|
+
let d = dirname(realOrNull(file) ?? file);
|
|
2136
|
+
for (let i = 0; i < 4; i++) {
|
|
2137
|
+
const pkg = readPackage(d);
|
|
2138
|
+
if (pkg) return { name: pkg.name ?? null, version: pkg.version ?? null };
|
|
2139
|
+
const up = dirname(d);
|
|
2140
|
+
if (up === d) break;
|
|
2141
|
+
d = up;
|
|
1982
2142
|
}
|
|
1983
2143
|
return null;
|
|
1984
2144
|
}
|
|
1985
2145
|
|
|
2146
|
+
/** The directory the installed copy of `spec.package` lives in under the engines folder `root`, or null. */
|
|
2147
|
+
function installedPackageDir(spec, root) {
|
|
2148
|
+
if (!spec.package || !root) return null;
|
|
2149
|
+
const dir = join(root, "node_modules", ...spec.package.split("/"));
|
|
2150
|
+
return existsSync(join(dir, "package.json")) ? dir : null;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** The program that installed copy declares for this engine, by the package's own `bin` field, or null. */
|
|
2154
|
+
function installedProgram(spec, root) {
|
|
2155
|
+
const dir = installedPackageDir(spec, root);
|
|
2156
|
+
if (!dir) return null;
|
|
2157
|
+
const pkg = readPackage(dir);
|
|
2158
|
+
const rel = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.[spec.fallback];
|
|
2159
|
+
return rel ? join(dir, rel) : null;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/** Can this candidate be spawned as the engine? `{ok: true, version}` or `{ok: false, why}`. */
|
|
2163
|
+
function engineCandidate(p, spec) {
|
|
2164
|
+
if (!isExecFile(p)) return { ok: false, why: "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)" };
|
|
2165
|
+
const own = owningPackage(p);
|
|
2166
|
+
const vendor = Boolean(spec.package) && own?.name === spec.package;
|
|
2167
|
+
if (vendor && !runsDirectly(p)) {
|
|
2168
|
+
return { ok: false, why: `the placeholder ${spec.package} leaves when its install step did not run (npm was given `
|
|
2169
|
+
+ "--ignore-scripts, or the platform's native package is missing), so every stage would print an error and "
|
|
2170
|
+
+ "exit. Reinstall without --ignore-scripts" };
|
|
2171
|
+
}
|
|
2172
|
+
return { ok: true, version: vendor ? own.version : null };
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/**
|
|
2176
|
+
* Find the program an engine spawns. Never throws: refusing is the caller's decision (preflightEngineBinary).
|
|
2177
|
+
*
|
|
2178
|
+
* Returns `{engine, binEnv, bin, explicit, relative, resolved, source, version, windowsShim, skipped, rejected}`.
|
|
2179
|
+
* `resolved` is an absolute path or null. `source` is "explicit" | "path" | "installed" | null. `version` is
|
|
2180
|
+
* read from the copy's own package.json when npm installed it, and null otherwise, because nothing is
|
|
2181
|
+
* spawned to ask. `skipped` lists Windows copies passed over under WSL; `rejected` lists candidates that
|
|
2182
|
+
* could not run, each with its reason and the step that found it (`source`).
|
|
2183
|
+
*
|
|
2184
|
+
* `enginesDir`: undefined reads the engines folder (enginesFolder: ENGINES_DIR_ENV from THIS process, then
|
|
2185
|
+
* the default under the home directory); a directory looks there; null looks nowhere. It is read from the
|
|
2186
|
+
* process rather than from `env` because it describes this user's install, not the configuration being
|
|
2187
|
+
* asked about: doctor asks about the units' environment from a shell, and the folder is the same either
|
|
2188
|
+
* way, because the services run as user units under the same home. `wsl` and `onWindowsDrive` are
|
|
2189
|
+
* injectable for the reason shared/wsl.mjs gives.
|
|
2190
|
+
*/
|
|
2191
|
+
export function resolveEngineProgram(engine, { env = process.env, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
|
|
2192
|
+
const id = String(engine ?? "").trim().toLowerCase();
|
|
2193
|
+
const spec = ENGINE_BINARIES[id];
|
|
2194
|
+
const out = { engine: id, binEnv: spec?.env ?? null, bin: null, explicit: false, relative: false,
|
|
2195
|
+
resolved: null, source: null, version: null, windowsShim: false, skipped: [], rejected: [] };
|
|
2196
|
+
if (!spec) return out;
|
|
2197
|
+
const set = String(envFrom(env, spec.env) ?? "").trim();
|
|
2198
|
+
out.explicit = Boolean(set) && set !== spec.fallback;
|
|
2199
|
+
out.bin = out.explicit ? set : spec.fallback;
|
|
2200
|
+
const underWsl = wsl ?? isWsl({ env });
|
|
2201
|
+
const onDrive = onWindowsDrive ?? ((p) => ON_A_WINDOWS_DRIVE.test(p));
|
|
2202
|
+
const take = (p, source) => {
|
|
2203
|
+
const c = engineCandidate(p, spec);
|
|
2204
|
+
if (!c.ok) { out.rejected.push({ path: p, why: c.why, source }); return false; }
|
|
2205
|
+
Object.assign(out, { resolved: p, source, version: c.version });
|
|
2206
|
+
return true;
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
if (out.explicit && out.bin.includes("/")) {
|
|
2210
|
+
if (!isAbsolute(out.bin)) { out.relative = true; return out; }
|
|
2211
|
+
out.windowsShim = underWsl && onDrive(out.bin);
|
|
2212
|
+
take(out.bin, "explicit");
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// THE PATH WALK splits on the platform's own delimiter. This file's walk used to split on ":", which
|
|
2217
|
+
// tears a Windows PATH at every drive letter; the wizard's walk already used the delimiter.
|
|
2218
|
+
const installed = out.explicit ? null : installedProgram(spec, enginesDir !== undefined ? enginesDir : enginesFolder());
|
|
2219
|
+
const installedReal = installed ? realOrNull(installed) : null;
|
|
2220
|
+
for (const dir of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
2221
|
+
const p = join(dir, out.bin);
|
|
2222
|
+
if (!isExecFile(p)) continue;
|
|
2223
|
+
// UNDER WSL THE WINDOWS PATH IS APPENDED TO THIS ONE, so `claude` on a fresh WSL2 Ubuntu resolves to the
|
|
2224
|
+
// Windows build before any Linux install. It is executable, and it fails the proof turn as "not signed
|
|
2225
|
+
// in" because the credential it looks for is the Linux one. Passed over, and named for the caller to say.
|
|
2226
|
+
if (underWsl && onDrive(p)) { out.skipped.push(p); continue; }
|
|
2227
|
+
if (installedReal && realOrNull(p) === installedReal) continue;
|
|
2228
|
+
if (take(p, out.explicit ? "explicit" : "path")) return out;
|
|
2229
|
+
}
|
|
2230
|
+
if (installed) take(installed, "installed");
|
|
2231
|
+
return out;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
1986
2234
|
/**
|
|
1987
|
-
* Refuse a run whose engine
|
|
2235
|
+
* Refuse a run whose engine program cannot be found or run, or is written as a relative path.
|
|
1988
2236
|
*
|
|
1989
2237
|
* Throws, like preflightCredentials, and is called at the same door — pipelineInner, before the run
|
|
1990
|
-
* context is built. Returns {engine, binEnv, bin, resolved} when the
|
|
2238
|
+
* context is built. Returns {engine, binEnv, bin, resolved, source, version} when the program is usable:
|
|
2239
|
+
* the answer of resolveEngineProgram above, which every other reader asks too.
|
|
1991
2240
|
*
|
|
1992
2241
|
* An UNKNOWN CLEAROTRON_AI returns without checking rather than throwing a second, differently-worded
|
|
1993
2242
|
* version of gateway.selectEngine's error. One definition of "that is not an engine", and it is the
|
|
1994
2243
|
* registry's.
|
|
1995
2244
|
*/
|
|
1996
|
-
export function preflightEngineBinary(env = process.env, { platform = process.platform } = {}) {
|
|
2245
|
+
export function preflightEngineBinary(env = process.env, { platform = process.platform, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
|
|
1997
2246
|
// item 3 — NATIVE WINDOWS REFUSES BY NAME, BEFORE ANYTHING READS PATH.
|
|
1998
2247
|
//
|
|
1999
|
-
// INSTALL.md promises a native-Windows run "refuses at preflight"
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2248
|
+
// INSTALL.md promises a native-Windows run "refuses at preflight". Nothing implemented it, so what a
|
|
2249
|
+
// Windows user actually got was a PATH walk that split on ":", tore `C:\Users\…` in half at the drive
|
|
2250
|
+
// letter, and reported their `claude.cmd` as "not on PATH". The walk now splits on the platform's own
|
|
2251
|
+
// delimiter, and the refusal stands anyway, because its real grounds were never the lookup: a stage runs
|
|
2252
|
+
// as its own process group and is stopped by signalling that group, an immediate stop identifies the
|
|
2253
|
+
// process from /proc or ps, and the write-boundary hook is quoted for a POSIX shell. Native Windows has
|
|
2254
|
+
// none of that, and where the program is found changes none of it.
|
|
2004
2255
|
//
|
|
2005
|
-
// This fires FIRST
|
|
2006
|
-
// it says, because the value it quotes has already been destroyed by the split.
|
|
2256
|
+
// This fires FIRST so that no message about a PATH reaches a reader whose platform is the answer.
|
|
2007
2257
|
//
|
|
2008
2258
|
// `platform` is injectable so the refusal is testable off win32 — the population this protects is the
|
|
2009
2259
|
// one that cannot run this suite to find out.
|
|
2010
2260
|
if (platform === "win32") {
|
|
2011
|
-
throw new Error("[preflight] this engine does not run on native Windows.
|
|
2012
|
-
+ "
|
|
2013
|
-
+ "
|
|
2014
|
-
+ "
|
|
2015
|
-
+ "documented install path applies unchanged (#1149 item 3).");
|
|
2261
|
+
throw new Error("[preflight] this engine does not run on native Windows. Each stage runs as its own process "
|
|
2262
|
+
+ "group and is stopped by signalling that group, which native Windows cannot do, so a stopped stage would "
|
|
2263
|
+
+ "leave the tools it started still running. Run it under WSL2, or in the devcontainer (.devcontainer/), "
|
|
2264
|
+
+ "where the documented install path applies unchanged.");
|
|
2016
2265
|
}
|
|
2017
2266
|
const engine = (env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
|
|
2018
2267
|
const spec = ENGINE_BINARIES[engine];
|
|
@@ -2033,24 +2282,38 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
|
|
|
2033
2282
|
// `envFrom` is therefore BELT-AND-BRACES, not the repair: it makes this site correct on its own terms
|
|
2034
2283
|
// rather than correct because something upstream normalised the environment first — a coupling
|
|
2035
2284
|
// nothing at this site declares and nothing here could notice breaking.
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2285
|
+
const r = resolveEngineProgram(engine, { env, enginesDir, wsl, onWindowsDrive }); // injectable for the reason it gives
|
|
2286
|
+
const bin = r.bin;
|
|
2287
|
+
const setTo = String(envFrom(env, spec.env) ?? "").trim();
|
|
2288
|
+
const where = `${spec.env}${r.explicit ? "" : setTo
|
|
2289
|
+
? ` (set to its default "${spec.fallback}": the one on PATH, then the copy Clearotron installed)`
|
|
2290
|
+
: ` (unset — defaulting to "${spec.fallback}" on PATH, then the copy Clearotron installed)`}`;
|
|
2291
|
+
|
|
2292
|
+
if (r.relative) {
|
|
2040
2293
|
throw new Error(`[preflight] ${where} is the RELATIVE path "${bin}", which cannot work: the engine is `
|
|
2041
2294
|
+ "spawned with the RUN DIRECTORY as its cwd (#524), not the repo, so a relative command is looked "
|
|
2042
2295
|
+ `for inside the run. Give an absolute path — e.g. ${join(REPO_ROOT, bin)} — or a bare name on PATH.`);
|
|
2043
2296
|
}
|
|
2044
2297
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2298
|
+
if (!r.resolved) {
|
|
2299
|
+
const passedOver = r.rejected.map((x) => `${x.path} is ${x.why}`).join("; ");
|
|
2300
|
+
// "None installed" only when none was. An installed copy that cannot run is named in the passed-over
|
|
2301
|
+
// list with its reason, and the claim beside it would send the reader to install it again.
|
|
2302
|
+
const installedRefused = r.rejected.some((x) => x.source === "installed");
|
|
2303
|
+
// Under WSL the Windows copies on the appended PATH were passed over on purpose, and the reader's own
|
|
2304
|
+
// `which` still prints them, so the refusal names them and says why.
|
|
2305
|
+
const windows = r.skipped.length
|
|
2306
|
+
? `. Passed over because they sit on a Windows drive, and a Windows build cannot run a stage here: ${r.skipped.join(", ")}` : "";
|
|
2047
2307
|
throw new Error(`[preflight] the ${engine} engine cannot run: ${where} names "${bin}", which is `
|
|
2048
2308
|
+ (bin.includes("/")
|
|
2049
|
-
? "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)"
|
|
2050
|
-
: `not on PATH as an executable file (PATH=${env.PATH || "(empty)"})`
|
|
2309
|
+
? (r.rejected[0]?.why ?? "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)")
|
|
2310
|
+
: `not on PATH as an executable file (PATH=${env.PATH || "(empty)"})`
|
|
2311
|
+
+ (r.explicit || installedRefused ? "" : ", and Clearotron has not installed one")
|
|
2312
|
+
+ (passedOver ? `. Passed over: ${passedOver}` : "")
|
|
2313
|
+
+ windows)
|
|
2051
2314
|
+ ". Every stage of a run spawns it, so the run is refused now rather than at the first stage.");
|
|
2052
2315
|
}
|
|
2053
|
-
return { engine, binEnv: spec.env, bin, resolved };
|
|
2316
|
+
return { engine, binEnv: spec.env, bin, resolved: r.resolved, source: r.source, version: r.version };
|
|
2054
2317
|
}
|
|
2055
2318
|
|
|
2056
2319
|
// Report which deployment hostnames are unset, so the runner can say so out loud at activation.
|
|
@@ -2119,7 +2382,7 @@ export function preflightDeploymentUrls(env = process.env) {
|
|
|
2119
2382
|
const RUN_FREE_BYTES_FLOOR = 500e6;
|
|
2120
2383
|
|
|
2121
2384
|
/** Nearest ancestor of `p` that exists. statfs needs a real path, and on a first run NONE of
|
|
2122
|
-
* …/workspace-<agent>/studio/
|
|
2385
|
+
* …/workspace-<agent>/studio/clearance-search exists yet — measuring the leaf would throw ENOENT and land
|
|
2123
2386
|
* in the unmeasurable branch, which would disable this check on exactly the fresh installs it is for. */
|
|
2124
2387
|
function nearestExistingDir(p) {
|
|
2125
2388
|
let dir = p;
|
|
@@ -2161,7 +2424,7 @@ export function freeSpacePlan({ freeBytes, needBytes, path }) {
|
|
|
2161
2424
|
* shape of run that can proceed without one, so there is no exemption to write.
|
|
2162
2425
|
*
|
|
2163
2426
|
* MEASURES THE FILESYSTEM THAT WILL HOLD THE BYTES, which is the workspace root's, not `/` and not the
|
|
2164
|
-
* repo's. Run directories live under config.studioRoot (…/workspace-<agent>/studio/
|
|
2427
|
+
* repo's. Run directories live under config.studioRoot (…/workspace-<agent>/studio/clearance-search); the
|
|
2165
2428
|
* published report goes to poolRoot and the packet to outboxDir, which on a laptop are different
|
|
2166
2429
|
* filesystems again. A check aimed at the wrong mount passes while the right one is full, which is the
|
|
2167
2430
|
* silent-pass this exists to prevent — so the path is taken from the caller's studioRoot when the runner
|