clearotron 0.3.2-beta.7 → 0.3.2-beta.8
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 +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +76 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +26 -26
- package/driver/contract-vocabulary.mjs +44 -10
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- 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/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +24 -18
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +200 -23
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +18 -1
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- 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 +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +142 -64
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +27 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +15 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
package/driver/door-gates.mjs
CHANGED
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
import { resolveEffectiveProfile, recipeProseGuard, platformEntryErrors } from "./profiles.mjs";
|
|
42
42
|
import { wantsPortalRoute, PORTAL_ROUTE_UNAVAILABLE } from "./enqueue-schema.mjs";
|
|
43
43
|
import { gateResolvedPolicy, loadRecipes } from "./search-policy.mjs";
|
|
44
|
-
import {
|
|
44
|
+
import { resolveTerritories } from "./effective-scope.mjs";
|
|
45
|
+
import { uncoveredTerritories, registerReachRefusal } from "./register-coverage.mjs";
|
|
46
|
+
import { readFlagSnapshot, registerTerritoriesFor, registerLabelFor } from "./flag-snapshot.mjs";
|
|
45
47
|
import { config } from "./driver.config.mjs";
|
|
46
48
|
import { resolveRequest } from "./resolve-request.mjs";
|
|
47
49
|
import { checkResolvedProduct } from "./scope-rules.mjs";
|
|
@@ -56,10 +58,14 @@ import { checkResolvedProduct } from "./scope-rules.mjs";
|
|
|
56
58
|
* `readable` says whether the profile store answered at all — the input `checkClearanceScopeRules`
|
|
57
59
|
* needs to tell "this account has no default territories" apart from "we could not read the account".
|
|
58
60
|
*/
|
|
59
|
-
/** The wired register
|
|
60
|
-
* read the snapshot must still open
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/** The wired register as a door needs it: what it covers, and what to CALL it in a sentence a client
|
|
62
|
+
* reads. One read, both answers. Never throws — a door that cannot read the snapshot must still open,
|
|
63
|
+
* and `territories: undefined` is the fail-open answer every layer below already speaks. */
|
|
64
|
+
function snapshotRegister() {
|
|
65
|
+
try {
|
|
66
|
+
const snap = readFlagSnapshot(config.poolRootOrNull);
|
|
67
|
+
return { territories: registerTerritoriesFor(snap), label: registerLabelFor(snap) };
|
|
68
|
+
} catch { return { territories: undefined, label: null }; }
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export function resolveForDoor(job) {
|
|
@@ -92,8 +98,12 @@ export function resolveForDoor(job) {
|
|
|
92
98
|
* products.mjs and search-policy.mjs, for every door.
|
|
93
99
|
*/
|
|
94
100
|
export function gateResolvedRequest({ job = null, profile = null, resolved = null, readable = true } = {},
|
|
95
|
-
{ availability = true, registerTerritories = undefined } = {}) {
|
|
101
|
+
{ availability = true, registerTerritories = undefined, registerLabel = undefined } = {}) {
|
|
96
102
|
const out = { errors: [], warnings: [], byCheck: {} };
|
|
103
|
+
// Read at most once per gate call, and not at all when both arms were given their answer — a test
|
|
104
|
+
// driving a Signa-shaped deployment must not need a snapshot on disk to do it.
|
|
105
|
+
let snapshot;
|
|
106
|
+
const register = () => (snapshot ??= snapshotRegister());
|
|
97
107
|
// A resolution that could not be taken is not a refusal — see the fail-open note in the header.
|
|
98
108
|
if (!resolved) return out;
|
|
99
109
|
// A CLARIFY IS RELAYED VERBATIM. It is already an actionable sentence naming the selector that could
|
|
@@ -114,10 +124,34 @@ export function gateResolvedRequest({ job = null, profile = null, resolved = nul
|
|
|
114
124
|
//
|
|
115
125
|
// `undefined` is the fail-open answer at every layer below, so an unreadable or absent snapshot
|
|
116
126
|
// leaves this arm silent and the runner's wall still decides — the door rule stated in the header.
|
|
117
|
-
const terr = registerTerritories !== undefined ? registerTerritories :
|
|
127
|
+
const terr = registerTerritories !== undefined ? registerTerritories : register().territories;
|
|
118
128
|
const gateMsg = gateResolvedPolicy(resolved, { registerTerritories: terr });
|
|
119
129
|
if (gateMsg) { out.errors.push(gateMsg); return out; }
|
|
120
130
|
}
|
|
131
|
+
// ── A TERRITORY THIS REQUEST NAMES THAT THE WIRED REGISTER CANNOT SEARCH ──────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// OUTSIDE the availability block, and that placement is the whole correctness of this arm. The portal
|
|
134
|
+
// and plan_run pass `availability:false` because they word an unavailable PRODUCT in their own words
|
|
135
|
+
// (the header's staff-prose split) — so an arm written inside that block cannot fire at the two doors
|
|
136
|
+
// a client actually orders through, and it would fail to fire SILENTLY, green, on the case the ruling
|
|
137
|
+
// of 2026-09-17 is about. This refusal carries no switch name and no internal key, so unlike the
|
|
138
|
+
// availability twin it is the same sentence on every surface and needs no client-facing rewording.
|
|
139
|
+
//
|
|
140
|
+
// THE TERRITORIES ARE RESOLVED HERE, NOT THREADED IN. `resolveTerritories` is the same ladder
|
|
141
|
+
// effective-scope.mjs runs for the scope a door prints beside this refusal, so the two cannot
|
|
142
|
+
// disagree. Asking each of the six doors to pass its own scope would let one forget the argument and
|
|
143
|
+
// fail open in silence — the exact shape `register-coverage-doors.test.mjs` exists to catch one layer
|
|
144
|
+
// up. One call, in the one gate they all share, covers every door by construction.
|
|
145
|
+
//
|
|
146
|
+
// Fail-open on a throw, the door's rule: an unreadable profile store must not stop somebody searching.
|
|
147
|
+
{
|
|
148
|
+
let named = [];
|
|
149
|
+
try { named = resolveTerritories(job ?? {}, profile, resolved?.recipeScope ?? null).jurisdictions ?? []; } catch { named = []; }
|
|
150
|
+
const terr = registerTerritories !== undefined ? registerTerritories : register().territories;
|
|
151
|
+
const label = registerLabel !== undefined ? registerLabel : register().label;
|
|
152
|
+
const refusal = registerReachRefusal(uncoveredTerritories(named, terr), label);
|
|
153
|
+
if (refusal) { out.errors.push(refusal); return out; }
|
|
154
|
+
}
|
|
121
155
|
const gates = checkResolvedProduct({ job, profile, resolved, profileReadable: profile !== null && readable });
|
|
122
156
|
out.errors.push(...gates.errors);
|
|
123
157
|
out.warnings.push(...gates.warnings);
|
package/driver/driver.config.mjs
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
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
|
|
@@ -616,12 +617,12 @@ export const MODELS = {
|
|
|
616
617
|
gemini: "google/gemini-3.1-pro-preview",
|
|
617
618
|
"gemini-flash": "google/gemini-3-flash-preview",
|
|
618
619
|
"deepseek-v4-pro": "together/deepseek-ai/DeepSeek-V4-Pro",
|
|
619
|
-
// azure =
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
azure:
|
|
620
|
+
// azure = a legacy catalogue entry for an Azure GPT deployment. NO STAGE NAMES IT AND NO ENGINE RUNS IT:
|
|
621
|
+
// the Claude adapter refuses the alias and the codex adapter maps no tier onto it. So its target is a
|
|
622
|
+
// constant. The setting that used to override it was retired on 2026-09-15 and nothing reads it any
|
|
623
|
+
// more; the configuration reference lists it under the settings that do nothing. It is named only
|
|
624
|
+
// there, in prose, because a name written in a source file here counts as a name this build reads.
|
|
625
|
+
azure: "azure-openai/gpt-5.4",
|
|
625
626
|
};
|
|
626
627
|
|
|
627
628
|
// alias → full id; a value that's already a full provider/model id (contains "/") passes through.
|
|
@@ -659,6 +660,13 @@ export function resolveModel(model) {
|
|
|
659
660
|
* never a default. A null on either side makes the comparison UNKNOWN, and an unknown must never be
|
|
660
661
|
* recorded as a match — that is the absence-read-as-a-pass class this whole issue is about.
|
|
661
662
|
*/
|
|
663
|
+
// FABLE IS NOT A FAMILY HERE, ON PURPOSE. Nobody has seen what the wire reports for a fable turn, so an id
|
|
664
|
+
// naming fable stays unknown and its comparison can never manufacture a mismatch. Placing it was tried
|
|
665
|
+
// (2026-09-15) and refused turns that had always run: a fable request served by claude-sonnet-5, by
|
|
666
|
+
// claude-opus-5, by a deployment named after another tier or by a GPT id, an opus request served as
|
|
667
|
+
// claude-fable-5-1, and any tier served under a name beginning with the word. It was taken out again the
|
|
668
|
+
// same day. The report's model line still names Fable: servedModels (tokens.mjs) reads a fable request's tier
|
|
669
|
+
// itself, where only the report sees it, and never through this comparison.
|
|
662
670
|
const MODEL_FAMILY_RE = /(?:^|\/)(?:claude-)?(opus|sonnet|haiku)(?:[-.]|$)/i;
|
|
663
671
|
|
|
664
672
|
// — THE OPENAI SIDE, added when the codex path could first answer "what ran".
|
|
@@ -1910,26 +1918,32 @@ export const ENGINE_BINARIES = {
|
|
|
1910
1918
|
// `vendor` is the one word a person needs — the staff config page answers "which engine is
|
|
1911
1919
|
// running the searches", and `label` below is the MECHANISM, which is what took off that page.
|
|
1912
1920
|
vendor: "Anthropic",
|
|
1921
|
+
// `product` is the name a reader knows the program by, beside the vendor in setup's engine question
|
|
1922
|
+
// ("Claude, by Anthropic"). Not `fallback`, which is the command word and lower-case.
|
|
1923
|
+
product: "Claude",
|
|
1913
1924
|
env: "CLEAROTRON_CLAUDE_PATH", fallback: "claude",
|
|
1925
|
+
// The npm package that carries this program, and the oldest version setup installs: "this version or
|
|
1926
|
+
// newer", with no ceiling. Setup installs it into the engines folder (enginesFolder, below the table)
|
|
1927
|
+
// when the reader picks this engine, and the resolver uses it only when the machine has no copy of its
|
|
1928
|
+
// own. The package's own `bin` field names the program, so no path inside it is written down here.
|
|
1929
|
+
package: "@anthropic-ai/claude-code", floor: "2.1.270",
|
|
1930
|
+
// WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
|
|
1931
|
+
// declared by the vendor: the engines folder after a fresh install of this package into an empty
|
|
1932
|
+
// folder, on npm 10.9.8 and on 11.19.1, 2026-09-14. A later release can be larger or smaller, so setup
|
|
1933
|
+
// says "about". Re-measure when the floor moves.
|
|
1934
|
+
installMB: 214,
|
|
1935
|
+
// The licence setup states wherever it tells a reader what they are about to install or use, as the
|
|
1936
|
+
// vendor's package declares it: "SEE LICENSE IN README.md", Anthropic's own terms.
|
|
1937
|
+
licence: "proprietary third-party software",
|
|
1914
1938
|
label: "Anthropic — each stage runs as a headless `claude -p` turn",
|
|
1915
1939
|
module: "engine/anthropic-agent.mjs", adapter: "anthropicAgentEngine",
|
|
1916
1940
|
signIn: "run `claude` once in a terminal and complete the sign-in",
|
|
1917
1941
|
authEnv: "CLEAROTRON_AI_BILLING", apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
1918
1942
|
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" },
|
|
1943
|
+
// `install`, the command that puts this program in the engines folder, is written in below the table
|
|
1944
|
+
// from `package` and `floor`, so the command a reader is shown and the one setup runs cannot drift.
|
|
1945
|
+
// npm, NOT the vendor's shell installer: a command this product executes on someone's box has to be
|
|
1946
|
+
// one they can read in full before they answer, and a piped remote script is not.
|
|
1933
1947
|
// — the documented headless ending. `claude setup-token` walks the sign-in and
|
|
1934
1948
|
// prints a long-lived token; the stage subprocess env is a spread of the driver's — spawnEnv
|
|
1935
1949
|
// strips ONLY the API key under subscription — so a token in the env file reaches the CLI
|
|
@@ -1940,23 +1954,66 @@ export const ENGINE_BINARIES = {
|
|
|
1940
1954
|
},
|
|
1941
1955
|
"openai-agent": {
|
|
1942
1956
|
vendor: "OpenAI",
|
|
1957
|
+
product: "Codex",
|
|
1943
1958
|
env: "CLEAROTRON_CODEX_PATH", fallback: "codex",
|
|
1959
|
+
package: "@openai/codex", floor: "0.154.0",
|
|
1960
|
+
installMB: 324, // measured the same way and on the same day as Claude's, above
|
|
1961
|
+
licence: "third-party software under the Apache-2.0 licence", // the package's own "license" field
|
|
1944
1962
|
label: "OpenAI — each stage runs as a headless `codex exec` turn",
|
|
1945
1963
|
module: "engine/openai-agent.mjs", adapter: "openaiAgentEngine",
|
|
1946
1964
|
signIn: "run `codex login`",
|
|
1947
1965
|
authEnv: "CLEAROTRON_AI_BILLING", apiKeyEnv: "CODEX_API_KEY",
|
|
1948
1966
|
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
1967
|
// — codex's headless ending writes its own ~/.codex/auth.json; there is no
|
|
1955
1968
|
// token to capture into an env file, and inventing one would be a route nobody has driven.
|
|
1956
1969
|
headless: { cmd: "codex login --device-auth", tokenEnv: null },
|
|
1957
1970
|
},
|
|
1958
1971
|
};
|
|
1959
1972
|
|
|
1973
|
+
// ── WHERE SETUP INSTALLS AN ENGINE'S PROGRAM, AND THE COMMAND THAT DOES IT ─────────────────────────────
|
|
1974
|
+
//
|
|
1975
|
+
// Nothing is bundled into the package. Setup installs the ONE program the reader's engine runs, when they
|
|
1976
|
+
// say yes, as an ordinary npm project in a folder Clearotron owns under their home directory: npm then
|
|
1977
|
+
// fetches that platform's binary and nothing else, and needs no root. The folder is not on PATH, so a copy
|
|
1978
|
+
// the machine installs itself is found first (resolveEngineProgram). `clearotron update` runs the same
|
|
1979
|
+
// install again, which moves the program to the newest its vendor publishes: measured on npm 10.9.8 and
|
|
1980
|
+
// 11.19.1, re-running an install with a `>=` range over an older copy moves it to the newest, where
|
|
1981
|
+
// `npm update` would stop at the caret npm writes into the folder's package.json.
|
|
1982
|
+
|
|
1983
|
+
/** Where to install, and look for, the engine programs instead of the default folder. An EMPTY directory means none. */
|
|
1984
|
+
export const ENGINES_DIR_ENV = "CLEAROTRON_ENGINES_DIR";
|
|
1985
|
+
|
|
1986
|
+
/** The default engines folder as a reader types it. */
|
|
1987
|
+
const ENGINES_FOLDER_TYPED = "~/.local/share/clearotron/engines";
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* The folder setup installs engine programs into and the resolver's last step reads: ENGINES_DIR_ENV from
|
|
1991
|
+
* this process when set, otherwise `~/.local/share/clearotron/engines`. Under the home directory rather
|
|
1992
|
+
* than the install's own tree, because an update replaces that tree, and because every service runs as a
|
|
1993
|
+
* user unit under the same home as the setup that installed it. XDG_DATA_HOME is not consulted: a shell's
|
|
1994
|
+
* value does not reach the services, and the two would then look in different folders.
|
|
1995
|
+
*/
|
|
1996
|
+
export function enginesFolder({ env = process.env, home = homedir() } = {}) {
|
|
1997
|
+
return String(env[ENGINES_DIR_ENV] ?? "").trim() || join(home, ".local", "share", "clearotron", "engines");
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/** The npm arguments that install, or refresh, an engine's program in `dir`: "this version or newer". */
|
|
2001
|
+
export function engineInstallArgs(spec, dir = enginesFolder()) {
|
|
2002
|
+
return ["install", "--prefix", dir, "--no-fund", "--no-audit", `${spec.package}@>=${spec.floor}`];
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/** A shell word as a reader pastes it: quoted when it must be, because a bare `>=` is a redirection. */
|
|
2006
|
+
const shellWord = (w) => (/^[\w@%+=:,./~-]+$/.test(w) ? w : `'${String(w).replace(/'/g, "'\\''")}'`);
|
|
2007
|
+
|
|
2008
|
+
/** The same install as a command a reader can paste. The default folder is written the way they type it. */
|
|
2009
|
+
export function engineInstallCommand(spec, dir = ENGINES_FOLDER_TYPED) {
|
|
2010
|
+
return ["npm", ...engineInstallArgs(spec, dir)].map(shellWord).join(" ");
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// The printable command each engine carries, for the readers that show one without a folder to hand (the
|
|
2014
|
+
// portal's engine page, doctor's way out of demo mode), made from the same parts as the install setup runs.
|
|
2015
|
+
for (const spec of Object.values(ENGINE_BINARIES)) spec.install = engineInstallCommand(spec);
|
|
2016
|
+
|
|
1960
2017
|
/** The production default, in ONE place rather than a literal repeated at every reader. */
|
|
1961
2018
|
export const DEFAULT_ENGINE_ID = "anthropic-agent";
|
|
1962
2019
|
|
|
@@ -1971,48 +2028,190 @@ export function engineAdapterSpecifier(engine) {
|
|
|
1971
2028
|
return spec ? pathToFileURL(join(DRIVER_DIR, spec.module)).href : null;
|
|
1972
2029
|
}
|
|
1973
2030
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2031
|
+
// ── WHERE AN ENGINE'S PROGRAM IS FOUND: ONE PLACE, AND EVERY READER ASKS IT ─────────────────────────────
|
|
2032
|
+
//
|
|
2033
|
+
// The run door, the inventory the portal reads, the wizard, doctor and both adapters all ask this one
|
|
2034
|
+
// function. Before it there were four answers: this file's PATH walk, the wizard's own walk (the only one
|
|
2035
|
+
// that passed over a Windows copy under WSL), and each adapter handing spawn(2) a bare word so the OS made
|
|
2036
|
+
// its own choice. They agreed only while every copy lived on PATH.
|
|
2037
|
+
//
|
|
2038
|
+
// THE ORDER, and why the machine's own copy wins:
|
|
2039
|
+
// 1. The explicit setting (`CLEAROTRON_CLAUDE_PATH` / `CLEAROTRON_CODEX_PATH`). A value that is set and
|
|
2040
|
+
// unusable is REPORTED, never overruled: the reader stated it, and quietly resolving somewhere else
|
|
2041
|
+
// would run a program nobody chose. The engine's own fallback word (`claude`, `codex`) is the default
|
|
2042
|
+
// spelled out, which is how the shipped example files write it, so it means exactly what unset means.
|
|
2043
|
+
// 2. The program on PATH: the machine's own install, which keeps updating itself.
|
|
2044
|
+
// 3. The copy setup installed in the engines folder (enginesFolder, above), only when the machine has
|
|
2045
|
+
// none. That folder is not on PATH. If a reader puts its node_modules/.bin there, a PATH hit that IS
|
|
2046
|
+
// that copy is passed over in step 2 and taken for what it is in step 3, so it is never reported, or
|
|
2047
|
+
// written into a settings file, as the machine's own.
|
|
2048
|
+
//
|
|
2049
|
+
// FILESYSTEM ONLY, like the rest of this door (see the header above ENGINE_BINARIES): nothing is spawned.
|
|
2050
|
+
|
|
2051
|
+
/** A path on a Windows drive as WSL mounts it. */
|
|
2052
|
+
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
2053
|
+
|
|
2054
|
+
const isExecFile = (p) => { try { return statSync(p).isFile() && (accessSync(p, X_OK), true); } catch { return false; } };
|
|
2055
|
+
const realOrNull = (p) => { try { return realpathSync(p); } catch { return null; } };
|
|
2056
|
+
const readPackage = (dir) => { try { return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")); } catch { return null; } };
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Whether the kernel runs this file ITSELF: a native executable (ELF, Mach-O, a Windows PE) or a `#!` script.
|
|
2060
|
+
*
|
|
2061
|
+
* Asked only of a file inside the vendor's own package, for one measured reason. The Claude package ships
|
|
2062
|
+
* `bin/claude.exe` as a 500-byte shell placeholder and puts the real program over it in its install step.
|
|
2063
|
+
* With `--ignore-scripts`, or with the platform's native package missing, the placeholder stays. It is a
|
|
2064
|
+
* regular file with the execute bit, so the executable check accepts it, and spawn runs it through `sh`:
|
|
2065
|
+
* it prints "claude native binary not installed" and exits 1, at every stage. It has neither `#!` nor a
|
|
2066
|
+
* binary header, so this refuses it at the door instead. A file anywhere else is judged as before; a
|
|
2067
|
+
* reader's own wrapper script is not ours to second-guess.
|
|
2068
|
+
*/
|
|
2069
|
+
function runsDirectly(file) {
|
|
2070
|
+
const head = Buffer.alloc(4);
|
|
2071
|
+
let fd = null, n = 0;
|
|
2072
|
+
try { fd = openSync(file, "r"); n = readSync(fd, head, 0, 4, 0); }
|
|
2073
|
+
catch { return false; }
|
|
2074
|
+
finally { if (fd !== null) { try { closeSync(fd); } catch { /* nothing left to release */ } } }
|
|
2075
|
+
if (n >= 2 && head[0] === 0x23 && head[1] === 0x21) return true; // #!
|
|
2076
|
+
if (n >= 2 && head[0] === 0x4d && head[1] === 0x5a) return true; // MZ
|
|
2077
|
+
if (n < 4) return false;
|
|
2078
|
+
const word = head.readUInt32BE(0);
|
|
2079
|
+
return word === 0x7f454c46 // ELF
|
|
2080
|
+
|| [0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe].includes(word); // Mach-O, thin and universal
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/** The npm package a file belongs to: the nearest package.json within a few levels of its real path. */
|
|
2084
|
+
function owningPackage(file) {
|
|
2085
|
+
let d = dirname(realOrNull(file) ?? file);
|
|
2086
|
+
for (let i = 0; i < 4; i++) {
|
|
2087
|
+
const pkg = readPackage(d);
|
|
2088
|
+
if (pkg) return { name: pkg.name ?? null, version: pkg.version ?? null };
|
|
2089
|
+
const up = dirname(d);
|
|
2090
|
+
if (up === d) break;
|
|
2091
|
+
d = up;
|
|
1982
2092
|
}
|
|
1983
2093
|
return null;
|
|
1984
2094
|
}
|
|
1985
2095
|
|
|
2096
|
+
/** The directory the installed copy of `spec.package` lives in under the engines folder `root`, or null. */
|
|
2097
|
+
function installedPackageDir(spec, root) {
|
|
2098
|
+
if (!spec.package || !root) return null;
|
|
2099
|
+
const dir = join(root, "node_modules", ...spec.package.split("/"));
|
|
2100
|
+
return existsSync(join(dir, "package.json")) ? dir : null;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
/** The program that installed copy declares for this engine, by the package's own `bin` field, or null. */
|
|
2104
|
+
function installedProgram(spec, root) {
|
|
2105
|
+
const dir = installedPackageDir(spec, root);
|
|
2106
|
+
if (!dir) return null;
|
|
2107
|
+
const pkg = readPackage(dir);
|
|
2108
|
+
const rel = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.[spec.fallback];
|
|
2109
|
+
return rel ? join(dir, rel) : null;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/** Can this candidate be spawned as the engine? `{ok: true, version}` or `{ok: false, why}`. */
|
|
2113
|
+
function engineCandidate(p, spec) {
|
|
2114
|
+
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)" };
|
|
2115
|
+
const own = owningPackage(p);
|
|
2116
|
+
const vendor = Boolean(spec.package) && own?.name === spec.package;
|
|
2117
|
+
if (vendor && !runsDirectly(p)) {
|
|
2118
|
+
return { ok: false, why: `the placeholder ${spec.package} leaves when its install step did not run (npm was given `
|
|
2119
|
+
+ "--ignore-scripts, or the platform's native package is missing), so every stage would print an error and "
|
|
2120
|
+
+ "exit. Reinstall without --ignore-scripts" };
|
|
2121
|
+
}
|
|
2122
|
+
return { ok: true, version: vendor ? own.version : null };
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* Find the program an engine spawns. Never throws: refusing is the caller's decision (preflightEngineBinary).
|
|
2127
|
+
*
|
|
2128
|
+
* Returns `{engine, binEnv, bin, explicit, relative, resolved, source, version, windowsShim, skipped, rejected}`.
|
|
2129
|
+
* `resolved` is an absolute path or null. `source` is "explicit" | "path" | "installed" | null. `version` is
|
|
2130
|
+
* read from the copy's own package.json when npm installed it, and null otherwise, because nothing is
|
|
2131
|
+
* spawned to ask. `skipped` lists Windows copies passed over under WSL; `rejected` lists candidates that
|
|
2132
|
+
* could not run, each with its reason and the step that found it (`source`).
|
|
2133
|
+
*
|
|
2134
|
+
* `enginesDir`: undefined reads the engines folder (enginesFolder: ENGINES_DIR_ENV from THIS process, then
|
|
2135
|
+
* the default under the home directory); a directory looks there; null looks nowhere. It is read from the
|
|
2136
|
+
* process rather than from `env` because it describes this user's install, not the configuration being
|
|
2137
|
+
* asked about: doctor asks about the units' environment from a shell, and the folder is the same either
|
|
2138
|
+
* way, because the services run as user units under the same home. `wsl` and `onWindowsDrive` are
|
|
2139
|
+
* injectable for the reason shared/wsl.mjs gives.
|
|
2140
|
+
*/
|
|
2141
|
+
export function resolveEngineProgram(engine, { env = process.env, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
|
|
2142
|
+
const id = String(engine ?? "").trim().toLowerCase();
|
|
2143
|
+
const spec = ENGINE_BINARIES[id];
|
|
2144
|
+
const out = { engine: id, binEnv: spec?.env ?? null, bin: null, explicit: false, relative: false,
|
|
2145
|
+
resolved: null, source: null, version: null, windowsShim: false, skipped: [], rejected: [] };
|
|
2146
|
+
if (!spec) return out;
|
|
2147
|
+
const set = String(envFrom(env, spec.env) ?? "").trim();
|
|
2148
|
+
out.explicit = Boolean(set) && set !== spec.fallback;
|
|
2149
|
+
out.bin = out.explicit ? set : spec.fallback;
|
|
2150
|
+
const underWsl = wsl ?? isWsl({ env });
|
|
2151
|
+
const onDrive = onWindowsDrive ?? ((p) => ON_A_WINDOWS_DRIVE.test(p));
|
|
2152
|
+
const take = (p, source) => {
|
|
2153
|
+
const c = engineCandidate(p, spec);
|
|
2154
|
+
if (!c.ok) { out.rejected.push({ path: p, why: c.why, source }); return false; }
|
|
2155
|
+
Object.assign(out, { resolved: p, source, version: c.version });
|
|
2156
|
+
return true;
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
if (out.explicit && out.bin.includes("/")) {
|
|
2160
|
+
if (!isAbsolute(out.bin)) { out.relative = true; return out; }
|
|
2161
|
+
out.windowsShim = underWsl && onDrive(out.bin);
|
|
2162
|
+
take(out.bin, "explicit");
|
|
2163
|
+
return out;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// THE PATH WALK splits on the platform's own delimiter. This file's walk used to split on ":", which
|
|
2167
|
+
// tears a Windows PATH at every drive letter; the wizard's walk already used the delimiter.
|
|
2168
|
+
const installed = out.explicit ? null : installedProgram(spec, enginesDir !== undefined ? enginesDir : enginesFolder());
|
|
2169
|
+
const installedReal = installed ? realOrNull(installed) : null;
|
|
2170
|
+
for (const dir of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
2171
|
+
const p = join(dir, out.bin);
|
|
2172
|
+
if (!isExecFile(p)) continue;
|
|
2173
|
+
// UNDER WSL THE WINDOWS PATH IS APPENDED TO THIS ONE, so `claude` on a fresh WSL2 Ubuntu resolves to the
|
|
2174
|
+
// Windows build before any Linux install. It is executable, and it fails the proof turn as "not signed
|
|
2175
|
+
// in" because the credential it looks for is the Linux one. Passed over, and named for the caller to say.
|
|
2176
|
+
if (underWsl && onDrive(p)) { out.skipped.push(p); continue; }
|
|
2177
|
+
if (installedReal && realOrNull(p) === installedReal) continue;
|
|
2178
|
+
if (take(p, out.explicit ? "explicit" : "path")) return out;
|
|
2179
|
+
}
|
|
2180
|
+
if (installed) take(installed, "installed");
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1986
2184
|
/**
|
|
1987
|
-
* Refuse a run whose engine
|
|
2185
|
+
* Refuse a run whose engine program cannot be found or run, or is written as a relative path.
|
|
1988
2186
|
*
|
|
1989
2187
|
* 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
|
|
2188
|
+
* context is built. Returns {engine, binEnv, bin, resolved, source, version} when the program is usable:
|
|
2189
|
+
* the answer of resolveEngineProgram above, which every other reader asks too.
|
|
1991
2190
|
*
|
|
1992
2191
|
* An UNKNOWN CLEAROTRON_AI returns without checking rather than throwing a second, differently-worded
|
|
1993
2192
|
* version of gateway.selectEngine's error. One definition of "that is not an engine", and it is the
|
|
1994
2193
|
* registry's.
|
|
1995
2194
|
*/
|
|
1996
|
-
export function preflightEngineBinary(env = process.env, { platform = process.platform } = {}) {
|
|
2195
|
+
export function preflightEngineBinary(env = process.env, { platform = process.platform, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
|
|
1997
2196
|
// item 3 — NATIVE WINDOWS REFUSES BY NAME, BEFORE ANYTHING READS PATH.
|
|
1998
2197
|
//
|
|
1999
|
-
// INSTALL.md promises a native-Windows run "refuses at preflight"
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2198
|
+
// INSTALL.md promises a native-Windows run "refuses at preflight". Nothing implemented it, so what a
|
|
2199
|
+
// Windows user actually got was a PATH walk that split on ":", tore `C:\Users\…` in half at the drive
|
|
2200
|
+
// letter, and reported their `claude.cmd` as "not on PATH". The walk now splits on the platform's own
|
|
2201
|
+
// delimiter, and the refusal stands anyway, because its real grounds were never the lookup: a stage runs
|
|
2202
|
+
// as its own process group and is stopped by signalling that group, an immediate stop identifies the
|
|
2203
|
+
// process from /proc or ps, and the write-boundary hook is quoted for a POSIX shell. Native Windows has
|
|
2204
|
+
// none of that, and where the program is found changes none of it.
|
|
2004
2205
|
//
|
|
2005
|
-
// This fires FIRST
|
|
2006
|
-
// it says, because the value it quotes has already been destroyed by the split.
|
|
2206
|
+
// This fires FIRST so that no message about a PATH reaches a reader whose platform is the answer.
|
|
2007
2207
|
//
|
|
2008
2208
|
// `platform` is injectable so the refusal is testable off win32 — the population this protects is the
|
|
2009
2209
|
// one that cannot run this suite to find out.
|
|
2010
2210
|
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).");
|
|
2211
|
+
throw new Error("[preflight] this engine does not run on native Windows. Each stage runs as its own process "
|
|
2212
|
+
+ "group and is stopped by signalling that group, which native Windows cannot do, so a stopped stage would "
|
|
2213
|
+
+ "leave the tools it started still running. Run it under WSL2, or in the devcontainer (.devcontainer/), "
|
|
2214
|
+
+ "where the documented install path applies unchanged.");
|
|
2016
2215
|
}
|
|
2017
2216
|
const engine = (env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
|
|
2018
2217
|
const spec = ENGINE_BINARIES[engine];
|
|
@@ -2033,24 +2232,38 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
|
|
|
2033
2232
|
// `envFrom` is therefore BELT-AND-BRACES, not the repair: it makes this site correct on its own terms
|
|
2034
2233
|
// rather than correct because something upstream normalised the environment first — a coupling
|
|
2035
2234
|
// nothing at this site declares and nothing here could notice breaking.
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2235
|
+
const r = resolveEngineProgram(engine, { env, enginesDir, wsl, onWindowsDrive }); // injectable for the reason it gives
|
|
2236
|
+
const bin = r.bin;
|
|
2237
|
+
const setTo = String(envFrom(env, spec.env) ?? "").trim();
|
|
2238
|
+
const where = `${spec.env}${r.explicit ? "" : setTo
|
|
2239
|
+
? ` (set to its default "${spec.fallback}": the one on PATH, then the copy Clearotron installed)`
|
|
2240
|
+
: ` (unset — defaulting to "${spec.fallback}" on PATH, then the copy Clearotron installed)`}`;
|
|
2241
|
+
|
|
2242
|
+
if (r.relative) {
|
|
2040
2243
|
throw new Error(`[preflight] ${where} is the RELATIVE path "${bin}", which cannot work: the engine is `
|
|
2041
2244
|
+ "spawned with the RUN DIRECTORY as its cwd (#524), not the repo, so a relative command is looked "
|
|
2042
2245
|
+ `for inside the run. Give an absolute path — e.g. ${join(REPO_ROOT, bin)} — or a bare name on PATH.`);
|
|
2043
2246
|
}
|
|
2044
2247
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2248
|
+
if (!r.resolved) {
|
|
2249
|
+
const passedOver = r.rejected.map((x) => `${x.path} is ${x.why}`).join("; ");
|
|
2250
|
+
// "None installed" only when none was. An installed copy that cannot run is named in the passed-over
|
|
2251
|
+
// list with its reason, and the claim beside it would send the reader to install it again.
|
|
2252
|
+
const installedRefused = r.rejected.some((x) => x.source === "installed");
|
|
2253
|
+
// Under WSL the Windows copies on the appended PATH were passed over on purpose, and the reader's own
|
|
2254
|
+
// `which` still prints them, so the refusal names them and says why.
|
|
2255
|
+
const windows = r.skipped.length
|
|
2256
|
+
? `. Passed over because they sit on a Windows drive, and a Windows build cannot run a stage here: ${r.skipped.join(", ")}` : "";
|
|
2047
2257
|
throw new Error(`[preflight] the ${engine} engine cannot run: ${where} names "${bin}", which is `
|
|
2048
2258
|
+ (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)"})`
|
|
2259
|
+
? (r.rejected[0]?.why ?? "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)")
|
|
2260
|
+
: `not on PATH as an executable file (PATH=${env.PATH || "(empty)"})`
|
|
2261
|
+
+ (r.explicit || installedRefused ? "" : ", and Clearotron has not installed one")
|
|
2262
|
+
+ (passedOver ? `. Passed over: ${passedOver}` : "")
|
|
2263
|
+
+ windows)
|
|
2051
2264
|
+ ". Every stage of a run spawns it, so the run is refused now rather than at the first stage.");
|
|
2052
2265
|
}
|
|
2053
|
-
return { engine, binEnv: spec.env, bin, resolved };
|
|
2266
|
+
return { engine, binEnv: spec.env, bin, resolved: r.resolved, source: r.source, version: r.version };
|
|
2054
2267
|
}
|
|
2055
2268
|
|
|
2056
2269
|
// Report which deployment hostnames are unset, so the runner can say so out loud at activation.
|
|
@@ -41,6 +41,8 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
|
|
|
41
41
|
sessionRef: string | null, // opaque resume handle (claude session_id | codex thread_id)
|
|
42
42
|
modelWire: string | null, // MODEL GAUGE — the served model id this turn observed (§3);
|
|
43
43
|
// null = nothing was observed, never the requested alias
|
|
44
|
+
providerWire: string | null, // PROVIDER GAUGE — the program's own word for who served the turn
|
|
45
|
+
// ("firstParty", "foundry"); null = not said, or said inconsistently
|
|
44
46
|
signals: { stalled?, noProgress?, hardWall?, rateLimited?, rateLimitBasis?, resetsAt?,
|
|
45
47
|
resetsAtBasis?, usageStreamed?, noStreamEvents?, thought: bool|null },
|
|
46
48
|
// THINKING GAUGE — see below.
|
|
@@ -139,8 +141,8 @@ Stages must name **abstract tiers**, not provider aliases. Per-engine maps:
|
|
|
139
141
|
|
|
140
142
|
| tier | role (stage examples) | anthropic-agent (claude alias) | openai-agent (codex `-m`) |
|
|
141
143
|
|---|---|---|---|
|
|
142
|
-
| `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `
|
|
143
|
-
| `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `
|
|
144
|
+
| `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `opus` | `$CLEAROTRON_OPENAI_MODEL_JUDGMENT` |
|
|
145
|
+
| `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `sonnet` | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
|
|
144
146
|
| `cheap` | saturation-probe | `haiku` | `$CLEAROTRON_OPENAI_MODEL_CHEAP` |
|
|
145
147
|
|
|
146
148
|
**AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** ( corruption 3, 2026-08-03). This
|
|
@@ -150,7 +152,12 @@ anthropic engine, "grade-moving, validated only in the paid A/B". The substituti
|
|
|
150
152
|
and could not be: the telemetry logged the alias that was ASKED FOR, so an arm run at gemini reported
|
|
151
153
|
gemini and ran sonnet. Both tiers are gone — the failover chain was deleted in and both stages
|
|
152
154
|
declare an anthropic tier in `STAGES` — and every engine's model map now **refuses** an alias it cannot
|
|
153
|
-
run (`claudeModel`, `openaiModel`).
|
|
155
|
+
run (`claudeModel`, `openaiModel`). On the anthropic engine a tier goes as the vendor's alias, a catalog
|
|
156
|
+
id in the table (`anthropic/claude-opus-5`) goes as itself, a bare or dated `claude-*` id goes as its
|
|
157
|
+
family's alias, and anything else throws. To hold a tier on one model, set the vendor's own
|
|
158
|
+
`ANTHROPIC_DEFAULT_OPUS_MODEL` / `_SONNET_MODEL` / `_HAIKU_MODEL`, or `ANTHROPIC_DEFAULT_FABLE_MODEL` for
|
|
159
|
+
`fable`, which no stage asks for unless an override names it, as `CLEAROTRON_SYNTHESIS_MODEL=fable` does; each
|
|
160
|
+
reaches the CLI through the stage's environment.
|
|
154
161
|
|
|
155
162
|
**Model provenance — two fields, never collapsed.** Every dispatch row (`_driver/<stage>.jsonl`) and
|
|
156
163
|
every `attempt` row (`_driver/run.jsonl`) carries:
|
package/driver/engine/README.md
CHANGED
|
@@ -13,7 +13,7 @@ does not exist — that is the design, and [`CONTRACT.md`](CONTRACT.md) is the d
|
|
|
13
13
|
| [`CONTRACT.md`](CONTRACT.md) | **The adapter contract.** What an engine must implement, the model-tier map, and what a turn is allowed to assume. Read this before either adapter |
|
|
14
14
|
| `anthropic-agent.mjs` | Spawns `claude -p`. Skill-reference absolutization, `--add-dir` grants, rate-limit and no-progress handling |
|
|
15
15
|
| `openai-agent.mjs` | Spawns `codex exec`. A per-run `CODEX_HOME` carrying a rendered `config.toml`, and the session-rollout reader that recovers the turn's usage |
|
|
16
|
-
| `auth.mjs` | `resolveAuthMode()` — subscription or
|
|
16
|
+
| `auth.mjs` | `resolveAuthMode()` — subscription, API key or (Claude only) cloud account, resolved once per turn and stamped on the telemetry |
|
|
17
17
|
| `probe.mjs` | Drives one cheap turn through whichever adapter is configured, to prove the engine can complete a turn at all. What `npm run setup` spends |
|
|
18
18
|
| `common.mjs` | Helpers both adapters share |
|
|
19
19
|
| `deny-authority-write.mjs` | A PreToolUse hook. `--add-dir` has no read-only form, so the read-only intent over the skills tree is enforced here |
|
|
@@ -27,7 +27,7 @@ succeeding on the other one. A run's manifest records which engine served it.
|
|
|
27
27
|
|
|
28
28
|
## Billing mode is resolved here, and it fails loud
|
|
29
29
|
|
|
30
|
-
`resolveAuthMode()` is the single place
|
|
30
|
+
`resolveAuthMode()` is the single place a turn's billing mode (subscription, API key or cloud account) is decided, and it is deliberately
|
|
31
31
|
unforgiving in one direction:
|
|
32
32
|
|
|
33
33
|
```
|