clearotron 0.3.0-beta.5 → 0.3.0-beta.7
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 +13 -3
- package/INSTALL.md +11 -0
- package/bin/connect.mjs +3 -1
- package/bin/onboard.mjs +38 -18
- package/bin/start.mjs +60 -2
- package/bin/update.mjs +38 -6
- package/build-info.json +2 -2
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/email-body.md +1 -1
- package/demo/knockout-search/run/status.json +2 -2
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/architecture/04-configuration-reference.md +2 -1
- package/driver/CHANGELOG.md +25 -0
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +65 -2
- package/driver/engine/openai-agent.mjs +8 -0
- package/driver/enqueue-schema.mjs +3 -1
- package/driver/gateway.mjs +12 -0
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +35 -3
- package/driver/portal-service.mjs +80 -18
- package/driver/portal-upstream.mjs +11 -1
- package/driver/search-policy.mjs +7 -2
- package/driver/suite-census.json +63 -27
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +3 -2
- package/mcp-server/lib/options.mjs +9 -4
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e.mjs +84 -12
- package/scripts/env-classify.mjs +3 -0
- package/shared/brand.mjs +7 -3
- package/shared/connect-clients.mjs +8 -0
- package/shared/names-in-force.mjs +1 -0
- package/shared/permanent-install.mjs +203 -0
- package/shared/stdio-connect.mjs +32 -14
- package/shared/verb-shim.mjs +10 -1
- package/shared/wsl.mjs +23 -0
package/.env.example
CHANGED
|
@@ -399,6 +399,15 @@ CLEAROTRON_DEMO=
|
|
|
399
399
|
# effect: deployment
|
|
400
400
|
# CLEAROTRON_INVOKED_AS=
|
|
401
401
|
|
|
402
|
+
# Marks an install that has already moved itself out of npm's npx cache. `npx clearotron install` first
|
|
403
|
+
# installs the same version under ~/.local and then runs that copy's install, with this set to 1, so the
|
|
404
|
+
# copy does not try to move itself a second time. Unset, an install running from npx's cache moves.
|
|
405
|
+
# Set by bin/onboard.mjs on the one process it starts, never by an operator, so the row is left
|
|
406
|
+
# commented. It is listed for the reason CLEAROTRON_DEMO is: the catalogue is about what an operator can
|
|
407
|
+
# FIND. Read by bin/onboard.mjs.
|
|
408
|
+
# effect: deployment
|
|
409
|
+
# CLEAROTRON_RELOCATED=
|
|
410
|
+
|
|
402
411
|
# How many client what-ifs the runner drains at once (default 1). A what-if re-runs ONE stage of a run
|
|
403
412
|
# that already exists, in a sandbox, at a client's request — the owner opened it to clients on
|
|
404
413
|
# 2026-08-27 and ruled SPEND controls out with it ("ignore the call spend"). This is not one.
|
|
@@ -552,8 +561,9 @@ CLEAROTRON_DEMO_PROFILES=
|
|
|
552
561
|
# Your organisation's name. `npm run setup` asks for it (and, on a local install, not for a sign-in
|
|
553
562
|
# address) and writes it here, quoted, so a name holding a `#` survives. The first `clearotron start` files it as the first
|
|
554
563
|
# organisation in the grants file (CLEAROTRON_ACCESS_FILE) when that file holds none; from then on the
|
|
555
|
-
# grants file holds the name
|
|
556
|
-
# organisation
|
|
557
|
-
#
|
|
564
|
+
# grants file holds the name and renaming is an edit there. The portal also reads it, to name the
|
|
565
|
+
# organisation on its top bar and on its sign-in refusal page. Unset or empty, no organisation is
|
|
566
|
+
# invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads
|
|
567
|
+
# it. Read by bin/start.mjs and shared/brand.mjs.
|
|
558
568
|
# effect: deployment
|
|
559
569
|
CLEAROTRON_ORGANISATION_NAME=
|
package/INSTALL.md
CHANGED
|
@@ -64,6 +64,15 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
64
64
|
npx clearotron install
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Run from `npx`, the install first installs Clearotron under `~/.local`, as `npm install -g --prefix
|
|
68
|
+
~/.local` would, and finishes from there. That way the `clearotron` command and your assistant's connection
|
|
69
|
+
do not point into npm's temporary cache, which npm replaces on an update and deletes when it cleans up.
|
|
70
|
+
Later, the `update` command updates that copy in place.
|
|
71
|
+
|
|
72
|
+
On WSL, a program on the Windows side can hold a port that WSL reports as free, and the browser reaches
|
|
73
|
+
it first. VS Code's Remote-SSH port forwarding is the common case. If the page that opens is not this
|
|
74
|
+
install's sign-in, run the same command again with `--port 28802`, or any free number.
|
|
75
|
+
|
|
67
76
|
A *hosted* deployment needs Linux for one further thing, the systemd outbox trigger —
|
|
68
77
|
[driver/systemd/README.md](driver/systemd/README.md).
|
|
69
78
|
- **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
|
|
@@ -754,6 +763,8 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
|
|
|
754
763
|
- Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
|
|
755
764
|
and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
|
|
756
765
|
of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
|
|
766
|
+
That does not move anything the env file already names: the saved-search lines above, and the data
|
|
767
|
+
directories `npx clearotron install` wrote, keep pointing at the old place until you edit them.
|
|
757
768
|
- Mints your sign-in passphrase and **prints it once**. Write it down. It is stored as a scrypt digest in
|
|
758
769
|
`~/trademark/portal-local-credential.json`, nothing can read it back, and no later start reprints it. To
|
|
759
770
|
get a new one, run `clearotron passphrase --reset`. An install that has been signing in with
|
package/bin/connect.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import { execFileSync } from "node:child_process";
|
|
|
47
47
|
import { createServer } from "node:net";
|
|
48
48
|
import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
49
49
|
import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
|
|
50
|
+
import { isWsl } from "../shared/wsl.mjs";
|
|
50
51
|
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
51
52
|
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
52
53
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
@@ -205,7 +206,8 @@ function deploymentHas(env = process.env) {
|
|
|
205
206
|
return {
|
|
206
207
|
// EVERY SHAPE, RESOLVED ONCE. A row picks its own; nothing here knows a client's name.
|
|
207
208
|
stdioRoutes: Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) =>
|
|
208
|
-
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null })])),
|
|
209
|
+
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null, reportsDir: env.CLEAROTRON_REPORTS_DIR || null })])),
|
|
210
|
+
wsl: isWsl({ env }),
|
|
209
211
|
// WHERE THE DOOR BINDS — not an address handed to any assistant. It is the loopback address the
|
|
210
212
|
// unit listens on, and `enablePlan` needs it to write the unit. It used to be passed to the
|
|
211
213
|
// resolver as `localAddress` and served to Cowork as somewhere to connect, which is the false
|
package/bin/onboard.mjs
CHANGED
|
@@ -68,6 +68,8 @@ import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor
|
|
|
68
68
|
import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
|
|
69
69
|
import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
|
|
70
70
|
import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
|
|
71
|
+
import { relocationPlan } from "../shared/permanent-install.mjs"; // — and the program out of npx's cache
|
|
72
|
+
import { isWsl } from "../shared/wsl.mjs"; // — one answer to "is this WSL", shared with the connect lines
|
|
71
73
|
import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
|
|
72
74
|
import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
73
75
|
// THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with `bin/start.mjs`. Two copies would be a
|
|
@@ -703,23 +705,8 @@ export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindows
|
|
|
703
705
|
/** A path on a Windows drive as WSL mounts it. */
|
|
704
706
|
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
705
707
|
|
|
706
|
-
/**
|
|
707
|
-
|
|
708
|
-
*
|
|
709
|
-
* BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects
|
|
710
|
-
* are the ones who cannot run this suite to find out, so a Linux runner has to be able to drive both
|
|
711
|
-
* answers rather than read the source and agree with it.
|
|
712
|
-
*
|
|
713
|
-
* A READ THAT FAILS ANSWERS "NOT WSL", and that is the direction that changes nothing: it leaves the
|
|
714
|
-
* resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start
|
|
715
|
-
* refusing candidates under /mnt on an ordinary Linux box with an ordinary mount.
|
|
716
|
-
*/
|
|
717
|
-
export function isWsl({ env = process.env, procVersion = null } = {}) {
|
|
718
|
-
if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
|
|
719
|
-
if (String(env.WSL_INTEROP ?? "").trim()) return true;
|
|
720
|
-
const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
|
|
721
|
-
return /microsoft|wsl/i.test(v);
|
|
722
|
-
}
|
|
708
|
+
/** Whether this is a Linux running under Windows: the one answer, from shared/wsl.mjs. */
|
|
709
|
+
export { isWsl };
|
|
723
710
|
|
|
724
711
|
/**
|
|
725
712
|
* What to say about candidates passed over because they sit on a Windows drive — or `null` when none
|
|
@@ -1860,7 +1847,7 @@ export async function runCheck() {
|
|
|
1860
1847
|
try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
|
|
1861
1848
|
if (unreadable) {
|
|
1862
1849
|
warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
|
|
1863
|
-
+ "to load, in the portal and the connector, until
|
|
1850
|
+
+ "to load, in the portal and the connector, until it is fixed");
|
|
1864
1851
|
} else ok(`saved searches are read from ${recipesDir}${handedBy}, and saves are committed in ${reach.repo}`);
|
|
1865
1852
|
}
|
|
1866
1853
|
}
|
|
@@ -2933,6 +2920,39 @@ if (!input.isTTY) {
|
|
|
2933
2920
|
process.exit(2);
|
|
2934
2921
|
}
|
|
2935
2922
|
|
|
2923
|
+
// ── OUT OF NPX'S CACHE, BEFORE ANYTHING IS WRITTEN ─────────────────────────────────────────────────────
|
|
2924
|
+
//
|
|
2925
|
+
// Run from npx, this program lives in npm's cache, and everything below would be wired to a directory npm
|
|
2926
|
+
// deletes: the launcher, and the connect line an assistant is registered with. So the install first puts
|
|
2927
|
+
// this same version somewhere permanent (shared/permanent-install.mjs) and runs itself from there. Nothing
|
|
2928
|
+
// has been written yet, so a failure here costs nothing, and it stops rather than carrying on: an install
|
|
2929
|
+
// finished from the cache is the defect, not a fallback.
|
|
2930
|
+
const move = relocationPlan();
|
|
2931
|
+
if (move && !move.skip && process.env.CLEAROTRON_RELOCATED !== "1") {
|
|
2932
|
+
say(`\n This is running from npm's temporary npx cache. Installing clearotron ${move.version} to ${move.prefix}`);
|
|
2933
|
+
say(" first, so the launcher and your assistants keep working after npm cleans that cache or you update.\n");
|
|
2934
|
+
// The npm that launched this, when npm says which: no second npm is guessed at.
|
|
2935
|
+
const npmCli = process.env.npm_execpath;
|
|
2936
|
+
const r = npmCli && existsSync(npmCli)
|
|
2937
|
+
? spawnSync(process.execPath, [npmCli, ...move.npmArgs], { stdio: "inherit" })
|
|
2938
|
+
: spawnSync("npm", move.npmArgs, { stdio: "inherit" });
|
|
2939
|
+
if (r.status !== 0 || !existsSync(move.entry)) {
|
|
2940
|
+
console.error(`\n Could not install clearotron to ${move.prefix}${r.error ? ` (${r.error.message})` : ""}. Nothing was installed, and nothing of yours was changed.`);
|
|
2941
|
+
console.error(` Run \`npm install --global --prefix ${move.prefix} clearotron@${move.version}\`, then \`${join(move.prefix, "bin", "clearotron")} install\`.\n`);
|
|
2942
|
+
process.exit(1);
|
|
2943
|
+
}
|
|
2944
|
+
// THE REST OF THE INSTALL RUNS FROM THE PERMANENT COPY, with npm's marks of an npx arrival taken off, so
|
|
2945
|
+
// it prints the commands of the install it now is.
|
|
2946
|
+
const env = { ...process.env, CLEAROTRON_RELOCATED: "1" };
|
|
2947
|
+
for (const k of ["npm_command", "npm_lifecycle_event", "npm_execpath"]) delete env[k];
|
|
2948
|
+
const moved = spawnSync(process.execPath, [move.entry, "install", ...process.argv.slice(2)], { stdio: "inherit", env });
|
|
2949
|
+
process.exit(moved.status ?? 1);
|
|
2950
|
+
}
|
|
2951
|
+
if (move?.skip) {
|
|
2952
|
+
console.error(`\n Note: this is running from npm's temporary npx cache and cannot be moved out of it here (${move.skip}).`);
|
|
2953
|
+
console.error(" It will stop working when npm cleans that cache. `npm install -g clearotron` installs it permanently.\n");
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2936
2956
|
// A credential typed at a prompt is echoed by the terminal and then sits in scrollback, in tmux history,
|
|
2937
2957
|
// in whatever the reader pastes into a bug report. So the echo is muted while a secret is being typed:
|
|
2938
2958
|
// the output stream readline writes through drops everything while `muted` is set.
|
package/bin/start.mjs
CHANGED
|
@@ -111,6 +111,7 @@ async function runTables() {
|
|
|
111
111
|
import { spawn, spawnSync, execFileSync } from "node:child_process";
|
|
112
112
|
import { storeInRepo, storeOutsideRepoMessage, storeCommitRefusal } from "../shared/store-in-repo.mjs"; //
|
|
113
113
|
import { stdioConnectOffer } from "../shared/stdio-connect.mjs";
|
|
114
|
+
import { demoProgramPlan } from "../shared/permanent-install.mjs"; // — a demo from npx keeps its own copy
|
|
114
115
|
import { mergeEnvFile } from "../shared/env-file-merge.mjs";
|
|
115
116
|
import { mcpOriginFor } from "../shared/lane-address.mjs"; // — one author for the origin
|
|
116
117
|
import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "../shared/server-units.mjs"; // — one authority, two callers
|
|
@@ -300,6 +301,22 @@ export function resolvePorts(env = {}) {
|
|
|
300
301
|
client: one("CLIENT_MCP_HTTP_PORT", clientDoorPort({})) };
|
|
301
302
|
}
|
|
302
303
|
|
|
304
|
+
/**
|
|
305
|
+
* WHAT TO DO WHEN THE PAGE THAT OPENS IS NOT OURS. Printed under every "Open" line.
|
|
306
|
+
*
|
|
307
|
+
* A port can be free where this runs and taken where the browser runs: on WSL, a Windows-side listener
|
|
308
|
+
* (VS Code's Remote-SSH forwarding is the one measured, 2026-09-11) answers 127.0.0.1 before WSL does. The
|
|
309
|
+
* doors bind cleanly, the in-use detection has nothing to see, and the browser shows somebody else's page
|
|
310
|
+
* with nothing on this screen saying so. `--port` already moves all three doors; the reader has to be told
|
|
311
|
+
* about it at the moment the address is handed over, which is here.
|
|
312
|
+
*/
|
|
313
|
+
export function foreignPageHint(verb) {
|
|
314
|
+
return [
|
|
315
|
+
"If the page that opens is not this install's sign-in, another program on this machine holds that",
|
|
316
|
+
`port from outside this environment. Run \`${invoke(verb)} --port 28802\` (or any free number) instead.`,
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
303
320
|
/**
|
|
304
321
|
* Apply `--port <n>` to the three doors.
|
|
305
322
|
*
|
|
@@ -740,6 +757,10 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
|
|
|
740
757
|
// already wrong for a process that is not the portal.
|
|
741
758
|
...(demo ? {
|
|
742
759
|
CLEAROTRON_DEMO: "1",
|
|
760
|
+
// A DEMO NAMES NO ORGANISATION. The portal reads this to name the one running the install, on its
|
|
761
|
+
// top bar and its sign-in refusal page, and an exported value is the reader's real organisation.
|
|
762
|
+
// The portal's alone: no door names an organisation, and the doors stay as a live install's.
|
|
763
|
+
CLEAROTRON_ORGANISATION_NAME: "",
|
|
743
764
|
// AND THE SIGN-IN CREDENTIAL LIVES IN THE DEMO'S OWN BASE. Without this it defaults to
|
|
744
765
|
// ~/.cordillera/portal-local-credential.json — shared with every install on the box — and the
|
|
745
766
|
// demo then inherits a digest minted for somebody else's address: the portal prints "the
|
|
@@ -1370,8 +1391,13 @@ if (isMain) {
|
|
|
1370
1391
|
// SEEDED FROM A COPY, for the reason the player publishes from one: republishing writes a receipt
|
|
1371
1392
|
// into the run directory it reads, and `demo/` is tracked. This is the path a reader actually takes
|
|
1372
1393
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1373
|
-
const { publishSource } = await import("../driver/demo-container.mjs");
|
|
1394
|
+
const { publishSource, seedDemoRuns } = await import("../driver/demo-container.mjs");
|
|
1374
1395
|
const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
|
|
1396
|
+
// AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
|
|
1397
|
+
// the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
|
|
1398
|
+
// links are stamped with this portal's address, the one the Open line prints.
|
|
1399
|
+
const runs = seedDemoRuns({ workspace: paths.workspace, examplesDir: join(REPO, "demo"), portalOrigin: `http://${HOST}:${ports.portal}` });
|
|
1400
|
+
if (runs.seeded.length) say(` runs ${runs.seeded.length} sample run(s) your assistant can list, brief and open`);
|
|
1375
1401
|
// WHAT WAS ALREADY THERE IS SAID TOO. This branch used to run only when the pool
|
|
1376
1402
|
// was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
|
|
1377
1403
|
// about what was MISSING and says nothing on its own about how many are now listed.
|
|
@@ -1431,6 +1457,33 @@ if (isMain) {
|
|
|
1431
1457
|
// the file the verb resets is the file the portal reads. A demo keeps its own, by layout (childEnv).
|
|
1432
1458
|
const { installCredential } = await import("../driver/portal-local-auth.mjs");
|
|
1433
1459
|
const signIn = DEMO ? null : installCredential({ base: paths.base, env: process.env, firstStart: firstStartOfThisInstall });
|
|
1460
|
+
// ── A DEMO RUN FROM NPX KEEPS ITS OWN COPY OF THE PROGRAM ───────────────────────────────────────────
|
|
1461
|
+
//
|
|
1462
|
+
// Its connect line launched the connector from npm's cache, which npm deletes when it cleans up, so an
|
|
1463
|
+
// assistant registered with it lost the demo without a word (measured on a published beta, 2026-09-11).
|
|
1464
|
+
// The same version goes into `<base>/program` (shared/permanent-install.mjs), BEFORE the services start
|
|
1465
|
+
// so the portal's connect rows name it too, and only when it is not there already. It never stops the
|
|
1466
|
+
// demo: if npm fails, the demo runs as before and says the line will not survive a cache clean.
|
|
1467
|
+
let demoProgramRoot = null;
|
|
1468
|
+
if (DEMO) {
|
|
1469
|
+
const plan = demoProgramPlan({ base: paths.base });
|
|
1470
|
+
if (plan?.current) demoProgramRoot = plan.root;
|
|
1471
|
+
else if (plan && !plan.skip) {
|
|
1472
|
+
say(` program copying clearotron ${plan.version} into ${plan.prefix}, so your assistant's connection survives npm cleaning its cache`);
|
|
1473
|
+
// The npm that launched this, when npm says which: no second npm is guessed at.
|
|
1474
|
+
const npmCli = process.env.npm_execpath;
|
|
1475
|
+
const r = npmCli && existsSync(npmCli)
|
|
1476
|
+
? spawnSync(process.execPath, [npmCli, ...plan.npmArgs], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8", timeout: 180_000 })
|
|
1477
|
+
: spawnSync("npm", plan.npmArgs, { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8", timeout: 180_000 });
|
|
1478
|
+
if (r.status === 0 && existsSync(join(plan.root, "mcp-server", "server.mjs"))) demoProgramRoot = plan.root;
|
|
1479
|
+
else {
|
|
1480
|
+
const why = r.error ? r.error.message : String(r.stderr ?? "").trim().split("\n").pop() || `npm exited ${r.status}`;
|
|
1481
|
+
say(` program could not be copied (${why}). The connect line below runs from npm's temporary cache,`);
|
|
1482
|
+
say(" so it stops working when npm cleans that cache; start the demo again to retry.");
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1434
1487
|
const envs = childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken,
|
|
1435
1488
|
localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null,
|
|
1436
1489
|
credential: signIn && signIn.source !== "shared" ? signIn.path : null });
|
|
@@ -1879,6 +1932,7 @@ if (isMain) {
|
|
|
1879
1932
|
}
|
|
1880
1933
|
say("");
|
|
1881
1934
|
say(` Open: ${envs.url}`);
|
|
1935
|
+
for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
|
|
1882
1936
|
say(" This SURVIVES the terminal — close the window, the product keeps running.");
|
|
1883
1937
|
say(` Stop it: ${invoke("stop")} (stops and removes the units; issued connect keys survive — \`${invoke("disconnect")}\` revokes those)`);
|
|
1884
1938
|
say(` Is it up? ${invoke("status")}`);
|
|
@@ -2112,6 +2166,7 @@ if (isMain) {
|
|
|
2112
2166
|
|
|
2113
2167
|
say("");
|
|
2114
2168
|
say(` Open ${envs.url}`);
|
|
2169
|
+
for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
|
|
2115
2170
|
say("");
|
|
2116
2171
|
// ── TWO DOORS, TWO AUDIENCES, BOTH NAMED ( — F26) ─────────────────────────
|
|
2117
2172
|
//
|
|
@@ -2209,7 +2264,10 @@ if (isMain) {
|
|
|
2209
2264
|
//
|
|
2210
2265
|
// The string comes from the ONE composer, not from a literal here: three surfaces state this route
|
|
2211
2266
|
// and a line of instruction with more than one author drifts silently.
|
|
2212
|
-
|
|
2267
|
+
// THE WORKSPACE AND POOL THE SERVICES WERE HANDED, not this process's environment: a demo reads no env
|
|
2268
|
+
// file, so its own line named no workspace and the connector fell back to the real install's.
|
|
2269
|
+
// A demo run from npx names its own copy of the program, which a cache clean does not remove.
|
|
2270
|
+
const connect = stdioConnectOffer({ workDir: paths.workspace, reportsDir: paths.pool, ...(demoProgramRoot ? { installRoot: demoProgramRoot } : {}) });
|
|
2213
2271
|
say(" Connect your assistant to this install — one line, no address and no sign-in:");
|
|
2214
2272
|
say("");
|
|
2215
2273
|
say(` ${connect.command}`);
|
package/bin/update.mjs
CHANGED
|
@@ -59,6 +59,8 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-p
|
|
|
59
59
|
import { readEnvFile } from "./onboard.mjs";
|
|
60
60
|
import { invoke, invocationPrefix } from "../shared/invocation.mjs"; // — name a command the reader can actually type
|
|
61
61
|
import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // a pull cannot update an untracked bundle
|
|
62
|
+
import { packagedUpdate } from "../shared/permanent-install.mjs"; // — a packaged install updates at its own prefix
|
|
63
|
+
import { installShim, inspectShim, shimPath } from "../shared/verb-shim.mjs"; // — npm's link replaces the launcher
|
|
62
64
|
|
|
63
65
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
64
66
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
@@ -246,13 +248,22 @@ export async function update(argv = process.argv.slice(2)) {
|
|
|
246
248
|
// not succeed` — true, useless, and pointing at the wrong thing entirely. `update` is the verb a
|
|
247
249
|
// stranger reaches for, so the one install we expect most people to have must not be answered with
|
|
248
250
|
// a git error about a directory that was never a repository.
|
|
251
|
+
//
|
|
252
|
+
// A PACKAGED INSTALL NOW UPDATES ITSELF, the way it was installed: npm, at the prefix it lives under, on
|
|
253
|
+
// the channel it came from (shared/permanent-install.mjs). It runs AFTER the live-run refusal below,
|
|
254
|
+
// because npm replaces the program's files as surely as `npm ci` does. Only a layout that cannot be
|
|
255
|
+
// named is still refused.
|
|
256
|
+
let packaged = null;
|
|
249
257
|
if (!isGitCheckout()) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
packaged = packagedUpdate();
|
|
259
|
+
if (!packaged) {
|
|
260
|
+
console.error("\n This install is not a git checkout, so there is nothing to pull.");
|
|
261
|
+
console.error(" It was installed from a package rather than cloned, which is the ordinary way.");
|
|
262
|
+
console.error("\n Update it the way it was installed:\n");
|
|
263
|
+
console.error(" npm install -g clearotron@latest\n");
|
|
264
|
+
console.error(" Nothing was touched.");
|
|
265
|
+
return 4;
|
|
266
|
+
}
|
|
256
267
|
}
|
|
257
268
|
|
|
258
269
|
// ── AND THE SECOND REFUSAL: NOT OVER A LIVE RUN ──────────────────────────────────────────────
|
|
@@ -289,6 +300,27 @@ export async function update(argv = process.argv.slice(2)) {
|
|
|
289
300
|
return 4;
|
|
290
301
|
}
|
|
291
302
|
|
|
303
|
+
if (packaged) {
|
|
304
|
+
if (packaged.current) {
|
|
305
|
+
say(`\n This install is ${packaged.installed}, and nothing newer is published (${packaged.tag}: ${packaged.version}). Nothing was touched.\n`);
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
if (packaged.unread) say(`\n npm did not say which versions are published, so this follows the ${packaged.tag} channel.`);
|
|
309
|
+
say(`\n Updating this install at ${packaged.prefix} from ${packaged.installed ?? "an unreadable version"} to clearotron@${packaged.spec}.`);
|
|
310
|
+
const rc = runInCheckout("npm", packaged.npmArgs);
|
|
311
|
+
if (rc !== 0) return rc;
|
|
312
|
+
// npm puts its own link back at `<prefix>/bin/clearotron` on every install, over the launcher the
|
|
313
|
+
// install wrote, and that link runs whichever `node` is first on PATH. Put the launcher back, but only
|
|
314
|
+
// over npm's link or our own: anything else there was not ours before this update either.
|
|
315
|
+
const kind = inspectShim(shimPath()).kind;
|
|
316
|
+
if (kind === "npm-link" || kind === "ours" || kind === "ours-other-install") {
|
|
317
|
+
const shim = installShim();
|
|
318
|
+
if (!shim.ok) console.error(`\n The update worked, but the launcher at ${shim.path ?? "~/.local/bin/clearotron"} could not be written back: ${shim.detail}.`);
|
|
319
|
+
}
|
|
320
|
+
say("\n Updated. An assistant starts the new version the next time it launches Clearotron; restart the services for the portal.\n");
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
292
324
|
say("\n Configuration store is outside the checkout. Updating the product.");
|
|
293
325
|
for (const e of entries) say(` ${e.name}=${e.value} (${e.from})`);
|
|
294
326
|
if (!entries.length) {
|
package/build-info.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "
|
|
30
|
+
"url": "/portal/report/tmpdemo2014fullcountrysearch-venqori-2026-09-03-sample-capture/",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-03T18:06:17.281Z",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "
|
|
30
|
+
"url": "/portal/report/tmpdemo2014globalpreliminarysearch-venqori-2026-09-02-sample-capture/",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-02T17:33:15.625Z",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<p style="font-family:Calibri,sans-serif;font-size:11pt">Knockout trademark review — DEMO-2014-knockout-search: <b>1 mark</b>, worst band <b style="color:#2f55a4">Low</b>. <a href="
|
|
1
|
+
<p style="font-family:Calibri,sans-serif;font-size:11pt">Knockout trademark review — DEMO-2014-knockout-search: <b>1 mark</b>, worst band <b style="color:#2f55a4">Low</b>. <a href="/portal/report/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/">Open the full report</a> · <a href="/portal/report/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/audit.xlsx">audit workbook</a>.</p>
|
|
2
2
|
<ul style="font-family:Calibri,sans-serif;font-size:11pt"><li><b>VENQORI</b>: <b style="color:#2f55a4">Low</b> — Classes 9, 42, 41 — Register filings (classes 9, 42, 41): 0 identical, 0 containing, 0 on close variations.</li></ul>
|
|
3
3
|
<p style="font-family:Calibri,sans-serif;font-size:11pt"><i>Ratings reflect our common law assessment. Register analysis may adjust ratings in either direction.</i></p>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"stepN": 6,
|
|
34
34
|
"stepTotal": 6,
|
|
35
35
|
"verdict": "Low",
|
|
36
|
-
"url": "
|
|
36
|
+
"url": "/portal/report/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/",
|
|
37
37
|
"failedStage": null,
|
|
38
38
|
"reason": null,
|
|
39
39
|
"deliveredAt": "2026-09-02T21:46:53.238Z",
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
"reports": [
|
|
255
255
|
{
|
|
256
256
|
"mark": "VENQORI",
|
|
257
|
-
"url": "
|
|
257
|
+
"url": "/portal/report/tmpdemo2014knockoutsearch-venqori-2026-09-02-sample-capture/"
|
|
258
258
|
}
|
|
259
259
|
],
|
|
260
260
|
"sendPending": true,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"stepTotal": 9,
|
|
28
28
|
"lastStage": "publish",
|
|
29
29
|
"verdict": "CONDITIONAL",
|
|
30
|
-
"url": "
|
|
30
|
+
"url": "/portal/report/tmpdemo2014multicountryfocussearch-venqori-2026-09-02-sample-capture/",
|
|
31
31
|
"failedStage": null,
|
|
32
32
|
"reason": null,
|
|
33
33
|
"deliveredAt": "2026-09-02T19:51:48.801Z",
|
|
@@ -285,12 +285,13 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
|
|
|
285
285
|
| `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
|
|
286
286
|
| `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
287
287
|
| `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo account, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` accounts either way. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
288
|
-
| `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install`, which asks for it and not for a sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name
|
|
288
|
+
| `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install`, which asks for it and not for a sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name and renaming is an edit there. The portal also reads it, to name the organisation on its top bar and on its sign-in refusal page. Unset ⇒ no organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads it. Effect class `deployment`; the full contract is its row in `.env.example`. |
|
|
289
289
|
| `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `clearotron start` points it inside the install's own base directory for a new install, so it mints its own passphrase instead of adopting a digest another install left; an install that has been signing in with the shared file keeps it. `npx clearotron demo` always points it inside the demo's own base directory and mints a new passphrase there on every start, so a demo never inherits a digest minted for another address, and removing the demo stays one `rm -rf`. |
|
|
290
290
|
| `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
|
|
291
291
|
| `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
|
|
292
292
|
| `PORTAL_OPS_TOKEN_FILE` | `~/.config/systemd/user/trademark-portal.service.d/secrets.conf` | The systemd drop-in the live-surface check reads `PORTAL_OPS_TOKEN` out of. It reads the FILE rather than the environment so a check run by hand sees the same token the service does. |
|
|
293
293
|
| `CLEAROTRON_INVOKED_AS` | unset (⇒ the verb's own `argv[1]`) | How the reader typed the command, so every command a verb prints for them to type next is spelled the way they type it: `clearotron …` after a global install, `npx clearotron …` otherwise. The dispatcher runs each verb as a process of its own, whose `argv[1]` is always `bin/<verb>.mjs`, so `bin/clearotron.mjs` passes its own `argv[1]` down in this name. **Set by the dispatcher, never by an operator.** Effect class `deployment`; the full contract is its row in `.env.example`. |
|
|
294
|
+
| `CLEAROTRON_RELOCATED` | unset (⇒ an install running from npx's cache moves itself to `~/.local` first) | Marks an install that has already made that move. `npx clearotron install` installs the same version under `~/.local`, then runs that copy's install with this set to `1`, so the copy does not try to move itself again. **Set by the install on the one process it starts, never by an operator.** Effect class `deployment`; the full contract is its row in `.env.example`. |
|
|
294
295
|
| `CLEAROTRON_REQUIRE_EXPLICIT_PORTS` | unset | `1` makes a service that would listen on a built-in default port refuse to start instead of warning. For a box that runs more than one instance, where one instance's default is another's port on the day that other instance is down. Unset, a service on a default port still says so as it starts. Effect class `deployment`. |
|
|
295
296
|
| `CLEAROTRON_UPDATER_STAMP` | `_updater-identity.json` beside the update script, in the directory the updater runs from | The full path of the file in which the updater that deploys this box records which copy of itself ran. The updater writes it and deploy health reads it under this one name, so a box that moves the stamp sets it once for both. On a box with no updater unit, setting it says an updater exists elsewhere and is to be judged. Effect class `deployment`. |
|
|
296
297
|
| `CLEAROTRON_CUT_REF` | `HEAD` | Which ref the cut decision reads the version from. **Read only by the release workflow, never set on a deployment.** The jobs that ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref and `HEAD` there is that ref rather than the branch they are deciding about. A job that asks the wrong subject gets a confident wrong answer. |
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c5744bf: Fixed: A demo started with npx keeps its assistant connection working after npm cleans its cache.
|
|
8
|
+
- c5744bf: Fixed: An assistant connected to the demo now opens each sample report on the demo's own portal, not on another server.
|
|
9
|
+
- c5744bf: Fixed: The portal names the organisation setup recorded, and the product keeps its own name beside it.
|
|
10
|
+
|
|
11
|
+
## 0.3.0-beta.6
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 2ca849d: Fixed: When codex's sign-in can no longer be refreshed, the search stops and says to run `codex login`. It used to retry with a bare exit code.
|
|
16
|
+
- a81279f: Fixed: When the portal address opens someone else's page, for example a port forwarded from outside WSL, Clearotron now says so and points you to `--port`.
|
|
17
|
+
- a81279f: Fixed: The sign-in page clears a session left by another Clearotron on the same address and says so. A refusal that is not about the passphrase now says what it is about.
|
|
18
|
+
- f133f7d: Fixed: A passphrase pasted with a space or line break at either end now signs in, instead of being refused as wrong.
|
|
19
|
+
- 2ca849d: Fixed: A manager who does not run the installation no longer sees server file paths when a new company cannot be recorded or filed.
|
|
20
|
+
- a81279f: Fixed: The local sign-in page no longer invites the browser to fill in a saved password from another install.
|
|
21
|
+
- a81279f: Fixed: `npx clearotron install` now installs Clearotron permanently under `~/.local` before setting up. The `clearotron` command and your assistants' connections keep working after npm cleans its cache.
|
|
22
|
+
- 2ca849d: Fixed: When the saved-search store exists but cannot be read, the connector and `clearotron doctor` now say so, instead of reporting no saved searches.
|
|
23
|
+
- a81279f: Fixed: On WSL, the "on this computer" connect steps now say to run them inside WSL. The Claude Code line registers Clearotron for every project and works in Windows PowerShell.
|
|
24
|
+
- a81279f: New: An assistant connected to `clearotron demo` can now list, brief and open the demo's four sample runs. They stay inside the demo's own folder.
|
|
25
|
+
- a81279f: Fixed: The sign-in page's reset line now runs for a demo started with npx, and resets that demo's own passphrase.
|
|
26
|
+
- a81279f: New: `clearotron update` now updates an npm-installed copy itself, and a beta install moves on to the release once it is published.
|
|
27
|
+
|
|
3
28
|
## 0.3.0-beta.5
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -221,7 +221,7 @@ export const E3_BACKLOG = [
|
|
|
221
221
|
where: "driver/skills/prelim-common-law/SKILL.md:192 (restated at driver/skills/prelim-search/synthesis-rules.md:394)",
|
|
222
222
|
surface: "skill-file",
|
|
223
223
|
evidence: "A clean PR/connotation row MUST cite its search — add a `**Connotation-search source:** <URL | \"perplexity_research — no result\">` line.",
|
|
224
|
-
reparsedBy: "driver/connotation-search.mjs — validators.commonLaw rejects a clean claim with no such line (connotation_search_missing); the hint is re-dictated
|
|
224
|
+
reparsedBy: "driver/connotation-search.mjs — validators.commonLaw rejects a clean claim with no such line (connotation_search_missing); the hint is re-dictated by correctionHint() in gateway.mjs",
|
|
225
225
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
226
226
|
},
|
|
227
227
|
{
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
// stage apart. This module is the single answer. `cut/` cannot import it (that directory does not travel
|
|
24
24
|
// and this one does), so the pack gate restates the disjunction and its own test pins the two together.
|
|
25
25
|
|
|
26
|
-
import { cpSync, existsSync, mkdtempSync, readdirSync } from "node:fs";
|
|
26
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
|
-
import { join, resolve, sep } from "node:path";
|
|
28
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
29
29
|
|
|
30
30
|
/** The entry file each lane's publisher reads as its source, in the order a child is probed for one. */
|
|
31
31
|
export const ENTRY_FILES = Object.freeze(["report.md", "knockout-findings.json"]);
|
|
@@ -78,3 +78,66 @@ export function publishSource(dir, { repoRoot, tmp = tmpdir() } = {}) {
|
|
|
78
78
|
cpSync(here, copy, { recursive: true });
|
|
79
79
|
return copy;
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* THE DEMO'S SAMPLE RUNS, WHERE AN ASSISTANT LOOKS FOR RUNS.
|
|
84
|
+
*
|
|
85
|
+
* The demo published its samples as reports and made no run directory, so the connector its own connect
|
|
86
|
+
* line wires listed nothing: an assistant, one of the demo's three faces, had nothing to explore. Each
|
|
87
|
+
* sample's finished `run/` is copied to the layout the connector walks, under the DEMO'S OWN workspace:
|
|
88
|
+
* `<workspace>/workspace-<agent>/studio/prelim-search/<slug>/<date>-<codename>/`. Nothing is written
|
|
89
|
+
* anywhere else, so a real install started afterwards sees none of it (the demo is its own install).
|
|
90
|
+
*
|
|
91
|
+
* Copied, never linked: a connector reading a run may write beside it, and `demo/` is tracked. A run
|
|
92
|
+
* already in place is not copied again, so a visitor's second start changes nothing but its links.
|
|
93
|
+
*
|
|
94
|
+
* ITS REPORT LINKS POINT AT THIS DEMO'S PORTAL. Each sample's `status.json` carried the report URL it was
|
|
95
|
+
* stamped with where it was captured, a test instance's address, and the connector hands a run's `url` to
|
|
96
|
+
* the assistant as-is, so an assistant asked to open a demo report sent the person to that host (measured
|
|
97
|
+
* on a published beta, 2026-09-11). The tracked samples now carry the portal's own route,
|
|
98
|
+
* `/portal/report/<runId>/`, with no host, and each copy here is stamped with `portalOrigin`, the demo
|
|
99
|
+
* portal's address. Stamped on EVERY start, the copies already in place included: `--port` moves the
|
|
100
|
+
* portal, and a copy laid down by an earlier version still carries the old host.
|
|
101
|
+
*/
|
|
102
|
+
export function seedDemoRuns({ workspace, examplesDir, portalOrigin = null }) {
|
|
103
|
+
const seeded = [], already = [];
|
|
104
|
+
for (const name of demoChildren(examplesDir)) {
|
|
105
|
+
const run = join(examplesDir, name, "run");
|
|
106
|
+
let s;
|
|
107
|
+
try { s = JSON.parse(readFileSync(join(run, "status.json"), "utf8")); } catch { continue; }
|
|
108
|
+
if (!s?.slug || !s?.codename || !s?.date) continue;
|
|
109
|
+
const dir = join(workspace, `workspace-${s.agent || "clawdi"}`, "studio", "prelim-search", s.slug, `${s.date}-${s.codename}`);
|
|
110
|
+
if (existsSync(join(dir, "status.json"))) already.push(s.runId);
|
|
111
|
+
else {
|
|
112
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
113
|
+
cpSync(run, dir, { recursive: true });
|
|
114
|
+
seeded.push(s.runId);
|
|
115
|
+
}
|
|
116
|
+
if (portalOrigin) stampReportLinks(join(dir, "status.json"), portalOrigin);
|
|
117
|
+
}
|
|
118
|
+
return { seeded, already };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The portal route that serves a run's report: the one `scanAccountRuns` hands the portal's own list. */
|
|
122
|
+
export const reportRoute = (runId) => `/portal/report/${encodeURIComponent(runId)}/`;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Point a seeded run's report links at `origin`. A link already on the portal's route keeps its path; any
|
|
126
|
+
* other (the host an older sample carried) becomes the run's own route. Written only when something moved.
|
|
127
|
+
*/
|
|
128
|
+
export function stampReportLinks(statusFile, origin) {
|
|
129
|
+
let s;
|
|
130
|
+
try { s = JSON.parse(readFileSync(statusFile, "utf8")); } catch { return false; }
|
|
131
|
+
if (!s?.runId) return false;
|
|
132
|
+
const base = String(origin).replace(/\/+$/, "");
|
|
133
|
+
const local = (url) => {
|
|
134
|
+
const path = String(url ?? "").replace(/^https?:\/\/[^/]+/, "");
|
|
135
|
+
return `${base}${path.startsWith("/portal/report/") ? path : reportRoute(s.runId)}`;
|
|
136
|
+
};
|
|
137
|
+
const next = { ...s, url: local(s.url) };
|
|
138
|
+
if (Array.isArray(s.reports)) next.reports = s.reports.map((r) => (r && typeof r === "object" ? { ...r, url: local(r.url) } : r));
|
|
139
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
140
|
+
if (text === `${JSON.stringify(s, null, 2)}\n`) return false;
|
|
141
|
+
writeFileSync(statusFile, text);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
@@ -412,6 +412,10 @@ export function readServedModel(codexHome, sinceMs = 0) {
|
|
|
412
412
|
// that cannot recur. This alternation is still a list of prose a vendor may change without notice,
|
|
413
413
|
// which is exactly why `rateLimitBasis: "text-match"` stays on the record.
|
|
414
414
|
const RATE_LIMIT_RE = /\b429\b|rate.?limit|usage limit|quota|too many requests|insufficient_quota/i;
|
|
415
|
+
// A SIGN-IN THAT CAN NO LONGER BE REFRESHED, in codex's own words (measured on a live install, 2026-09-11).
|
|
416
|
+
// Narrow on purpose: a register provider's 401 or 403 can land in a stage's stderr too, and reading that as
|
|
417
|
+
// the operator's codex login having expired would send them to sign in again for nothing.
|
|
418
|
+
const REFRESH_FAILED_RE = /refresh token (?:has|was) already been used|access token could not be refreshed|failed to refresh token/i;
|
|
415
419
|
|
|
416
420
|
// A retry hint, WHEN THE MESSAGE HAPPENS TO CARRY ONE. Two anchored shapes and nothing clever: an
|
|
417
421
|
// explicit timestamp, or a relative delay with a unit. Anything else yields undefined and the driver
|
|
@@ -484,6 +488,7 @@ function settleTuple({ r, ev, resumeRef }) {
|
|
|
484
488
|
// backoff (CLEAROTRON_RATE_LIMIT_DEFAULT_BACKOFF_MS), exactly as the anthropic no-reset 429 path does.
|
|
485
489
|
const rateLimitText = `${ev.turnFailed || ""}\n${ev.streamError || ""}\n${r.stderr || ""}`;
|
|
486
490
|
const rateLimited = !killed && RATE_LIMIT_RE.test(rateLimitText);
|
|
491
|
+
const signedOut = !killed && REFRESH_FAILED_RE.test(rateLimitText);
|
|
487
492
|
const resetsAt = rateLimited ? parseResetHint(rateLimitText) : undefined;
|
|
488
493
|
return {
|
|
489
494
|
code: killed ? 137 : (failed ? (r.rawCode || 1) : 0),
|
|
@@ -521,6 +526,9 @@ function settleTuple({ r, ev, resumeRef }) {
|
|
|
521
526
|
// receives and is not this adapter's call — but nothing downstream can any longer fail to know.
|
|
522
527
|
mcpRefused: mcpToolGauge(ev).mcpToolCallsRefused > 0 || undefined,
|
|
523
528
|
rateLimited: rateLimited || undefined,
|
|
529
|
+
// The engine's sign-in could not be refreshed: what the operator runs to fix it. The gateway names the
|
|
530
|
+
// stage's failure with it.
|
|
531
|
+
signedOut: signedOut ? "codex sign-in expired — run `codex login`, then start the search again" : undefined,
|
|
524
532
|
rateLimitBasis: rateLimited ? "text-match" : undefined,
|
|
525
533
|
// resetsAtBasis (2026-08-20): same honesty as rateLimitBasis one line up, for the reset
|
|
526
534
|
// CLOCK rather than the classification. codex states its reset as human prose with NO timezone
|
|
@@ -456,7 +456,9 @@ export function validateJob(job, { atClaim = false } = {}) {
|
|
|
456
456
|
if (projectKey) consulted.push(`the project ${JSON.stringify(projectKey)}`);
|
|
457
457
|
// The saved search is a rung above the project. `clarify` means the selector itself is wrong, which
|
|
458
458
|
// the recipeKey/product checks further down report properly — here it just means no scope to read.
|
|
459
|
-
|
|
459
|
+
// The store is read only for a job that names a saved search, as every other door reads it: one that
|
|
460
|
+
// cannot be read must not refuse a job that never asked for anything in it.
|
|
461
|
+
const resolved = resolveSearchPolicy(job, { profile, recipes: job.recipeKey ? loadRecipes({ force: true, proseGuard: recipeProseGuard }) : null });
|
|
460
462
|
if (job.recipeKey) consulted.push(`the saved search ${JSON.stringify(String(job.recipeKey))}`);
|
|
461
463
|
const scope = resolveEffectiveScope(job, profile, resolved?.clarify ? null : resolved);
|
|
462
464
|
inherited = Array.isArray(scope?.classes) ? scope.classes : [];
|