clearotron 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
package/bin/onboard.mjs
CHANGED
|
@@ -64,11 +64,16 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
64
64
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, chmodSync } from "node:fs"; // read the process table here; moved that to shared/process-table.mjs
|
|
65
65
|
import { homedir, userInfo } from "node:os";
|
|
66
66
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
|
|
67
|
+
import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor is package.json engines, not a constant here
|
|
67
68
|
import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
|
|
68
69
|
import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
|
|
69
70
|
import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
|
|
70
71
|
import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
|
|
71
|
-
import { bracketAsciiCells } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
72
|
+
import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
73
|
+
// ONE CLASSIFIER, shared with `bin/start.mjs`. The wizard asks the question; the launcher enforces the
|
|
74
|
+
// answer. Two copies of "what does this domain admit" is a wizard that consents to one rule and a
|
|
75
|
+
// launcher that builds another.
|
|
76
|
+
import { classifyStaffDomain, domainOfEmail, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
|
|
72
77
|
import { join, dirname, isAbsolute, resolve } from "node:path";
|
|
73
78
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
74
79
|
import { delimiter } from "node:path";
|
|
@@ -121,7 +126,6 @@ const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed
|
|
|
121
126
|
// command is applying. Writes stay on ENV_PATH: a writer that followed the file backwards would keep an
|
|
122
127
|
// install in the directory npm replaces forever.
|
|
123
128
|
const READ_ENV_PATH = () => activeEnvPath({ repoRoot: REPO });
|
|
124
|
-
const NODE_FLOOR = 22;
|
|
125
129
|
|
|
126
130
|
const argv = process.argv.slice(2);
|
|
127
131
|
const has = (n) => argv.includes(n);
|
|
@@ -587,30 +591,194 @@ export const AMBIENT_KEYS = [
|
|
|
587
591
|
// ── helpers ──────────────────────────────────────────────────────────────────────────────────────────
|
|
588
592
|
const present = (v) => typeof v === "string" && v.trim() !== "";
|
|
589
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Ask who signs in, and take a real yes for the access rule that address implies.
|
|
596
|
+
*
|
|
597
|
+
* ── WHY THIS QUESTION EXISTS AT ALL ─────────────────────────────────────────────────────────────────
|
|
598
|
+
*
|
|
599
|
+
* Setup never asked for an address. `clearotron start` therefore took one from `--user`, from
|
|
600
|
+
* `PORTAL_LOCAL_USER`, or from the local account as `<account>@localhost` — and derived a STAFF RULE
|
|
601
|
+
* from everything after its `@`. On the local-account default that rule is `localhost`: one machine,
|
|
602
|
+
* one identity, nothing granted to anyone. Given a real address it is that address's whole domain, and
|
|
603
|
+
* the settings page then reports it back as "Anyone at <domain> — a rule, not a person".
|
|
604
|
+
*
|
|
605
|
+
* An outside install reached exactly that state. Nobody typed the address into this wizard, because
|
|
606
|
+
* this wizard had no prompt for it; an assistant filled one into the environment file on the operator's
|
|
607
|
+
* behalf, and the install granted a documentation domain. They read their own settings page and
|
|
608
|
+
* reported it as a back door. They were right to: a grant to a group had been made, by nobody.
|
|
609
|
+
*
|
|
610
|
+
* So the address is asked for here, where a person is definitionally present, and the grant it implies
|
|
611
|
+
* is shown in the words the settings page uses BEFORE it is written. `start` no longer guesses: it
|
|
612
|
+
* refuses a rule wider than one machine unless `PORTAL_STAFF_DOMAINS` says so in writing, which is what
|
|
613
|
+
* a yes here writes.
|
|
614
|
+
*
|
|
615
|
+
* THE DEFAULT IS THE LOCAL ACCOUNT AND NOTHING ELSE — not the git author, not the hostname, not a shell
|
|
616
|
+
* variable. An identity that becomes an access rule comes from the person, or from the one source that
|
|
617
|
+
* cannot name a second person.
|
|
618
|
+
*
|
|
619
|
+
* `io` IS INJECTED for the reason `offerUsptoSync`'s is: the branch that matters is the one where a
|
|
620
|
+
* reader pressed Enter at the grant question and NOTHING must be granted, and closed over a terminal
|
|
621
|
+
* that branch is asserted nowhere. Returns the `.env` keys to write and nothing else.
|
|
622
|
+
*/
|
|
623
|
+
export async function askSignIn(io, { localAccount = "user", staffLabel = "Staff", envPath = "" } = {}) {
|
|
624
|
+
const { askValue, confirm, say = () => {}, ok = () => {}, info = () => {},
|
|
625
|
+
warn = () => {}, problem = () => {} } = io;
|
|
626
|
+
const localDefault = `${localAccount}@localhost`;
|
|
627
|
+
prose("The portal admits one address on this install, and that address is also what decides who is an",
|
|
628
|
+
"administrator. Enter accepts the local-account form, which is this machine and nobody else.");
|
|
629
|
+
for (;;) {
|
|
630
|
+
const typed = String(await askValue("Sign-in address:", { def: localDefault })).trim().toLowerCase();
|
|
631
|
+
if (!typed.includes("@") || typed.indexOf("@") !== typed.lastIndexOf("@")) {
|
|
632
|
+
problem(`"${typed}" is not a single email address. The portal refuses a multi-@ identity outright, `
|
|
633
|
+
+ "so this would sign in and then be denied at the door.");
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const domain = domainOfEmail(typed);
|
|
637
|
+
const verdict = classifyStaffDomain(domain);
|
|
638
|
+
if (verdict === "public" || verdict === "reserved") {
|
|
639
|
+
// The classifier's own sentence, never a second copy: `clearotron start` prints these same words
|
|
640
|
+
// when it meets the same domain, and two wordings of one refusal is how a reader comes to believe
|
|
641
|
+
// they have met two different problems.
|
|
642
|
+
problem(staffDomainRefusal(domain));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (verdict === "narrow") {
|
|
646
|
+
ok(`${typed} — this machine only. No domain rule is written, and nobody else is granted anything.`);
|
|
647
|
+
return { PORTAL_LOCAL_USER: typed };
|
|
648
|
+
}
|
|
649
|
+
// `wide`: a real domain, so the grant is a grant. Stated first, in the settings page's own words,
|
|
650
|
+
// and Enter is NOT a yes — the default is no, because this is the one answer in the wizard that
|
|
651
|
+
// admits people the reader has never met.
|
|
652
|
+
say("");
|
|
653
|
+
warn(`${typed} makes this a rule about ${domain}, not about you.`);
|
|
654
|
+
say(` ${staffGrantSentence(domain, { staffLabel })}`);
|
|
655
|
+
say("");
|
|
656
|
+
say(` Everyone at ${domain} who gets past this install's sign-in door would see every brand owner`);
|
|
657
|
+
say(" on it — every clearance, every report, every configuration. On a laptop that is only you,");
|
|
658
|
+
say(" because only one address can sign in. Behind a company login it is the whole domain.");
|
|
659
|
+
say("");
|
|
660
|
+
if (await confirm(`Grant ${domain} that, and write it down as PORTAL_STAFF_DOMAINS?`, false))
|
|
661
|
+
return { PORTAL_LOCAL_USER: typed, PORTAL_STAFF_DOMAINS: domain };
|
|
662
|
+
info(`nothing granted${envPath ? `, and nothing written to ${envPath}` : ""}. ${typed} would sign in `
|
|
663
|
+
+ "and every page would refuse it, because signing in is not being enrolled — so choose the "
|
|
664
|
+
+ "local-account form, or answer yes above.");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
590
668
|
/**
|
|
591
669
|
* An engine binary, resolved the way the engine resolves it — and the trap that resolution carries.
|
|
592
670
|
*
|
|
593
671
|
* Was `resolveClaudeBin`. The body never had anything claude-specific in it; the NAME was the last place
|
|
594
672
|
* this file still assumed one engine, and a name that lies is how the second adapter stayed invisible.
|
|
595
673
|
*/
|
|
596
|
-
export function resolveEngineBin(bin) {
|
|
674
|
+
export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindowsDrive = null } = {}) {
|
|
675
|
+
// The predicate, not just the pattern, because /mnt/c cannot be created on a Linux runner without
|
|
676
|
+
// root — so an arm that could only supply a PATH could never drive the skip, and the branch would
|
|
677
|
+
// ship asserted by nobody. `ON_A_WINDOWS_DRIVE` is held to real paths by its own arm.
|
|
678
|
+
const onDrive = onWindowsDrive ?? ((x) => ON_A_WINDOWS_DRIVE.test(x));
|
|
597
679
|
// driver/engine/anthropic-agent.mjs — `CLEAROTRON_CLAUDE_PATH || "claude"`; openai-agent.mjs — the same
|
|
598
680
|
// shape on `CLEAROTRON_CODEX_PATH || "codex"`. A RELATIVE path is the trap for BOTH: stage subprocesses are
|
|
599
681
|
// spawned with cwd set to the RUN DIRECTORY (driver/engine/common.mjs resolveSpawnCwd, shared by the
|
|
600
682
|
// two adapters), so a relative binary resolves against a directory that did not exist at setup time.
|
|
683
|
+
const underWsl = wsl ?? isWsl({ env });
|
|
601
684
|
if (bin.includes("/")) {
|
|
602
685
|
const abs = resolve(bin);
|
|
603
|
-
|
|
604
|
-
|
|
686
|
+
// A PATH SOMEBODY TYPED IS NOT OVERRULED, only reported. The reader stated this one, and silently
|
|
687
|
+
// resolving somewhere else would be the launcher moving a door off a port that was asked for.
|
|
688
|
+
return { path: abs, executable: isExec(abs), relative: !isAbsolute(bin), windowsShim: underWsl && onDrive(abs), skipped: [] };
|
|
605
689
|
}
|
|
606
|
-
|
|
690
|
+
// ── UNDER WSL, THE WINDOWS PATH IS APPENDED TO THIS ONE ────────────────────────────────────────
|
|
691
|
+
//
|
|
692
|
+
// So `claude` on a fresh WSL2 Ubuntu resolves to /mnt/c/…/claude — the WINDOWS shim — before any
|
|
693
|
+
// Linux install is reached, and it is executable by every test this makes. It then fails the proof
|
|
694
|
+
// turn as "not signed in", because the credential it is looking for is the Linux one, and a reader
|
|
695
|
+
// is sent to fix a sign-in that was never the problem. Reported from a real WSL2 attempt.
|
|
696
|
+
//
|
|
697
|
+
// SKIPPED, AND NAMED. Passing over a candidate silently would leave the reader with "no binary
|
|
698
|
+
// found" on a machine where `which claude` prints one, so the skips travel back for the caller to
|
|
699
|
+
// say out loud. A missing Linux install then reports as missing, which is the true answer.
|
|
700
|
+
const skipped = [];
|
|
701
|
+
for (const dir of (env.PATH || "").split(delimiter).filter(Boolean)) {
|
|
607
702
|
const p = join(dir, bin);
|
|
608
|
-
if (isExec(p))
|
|
703
|
+
if (!isExec(p)) continue;
|
|
704
|
+
if (underWsl && onDrive(p)) { skipped.push(p); continue; }
|
|
705
|
+
return { path: p, executable: true, relative: false, windowsShim: false, skipped };
|
|
609
706
|
}
|
|
610
|
-
return { path: null, executable: false, relative: false };
|
|
707
|
+
return { path: null, executable: false, relative: false, windowsShim: false, skipped };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** A path on a Windows drive as WSL mounts it. */
|
|
711
|
+
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Whether this is a Linux running under Windows.
|
|
715
|
+
*
|
|
716
|
+
* BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects
|
|
717
|
+
* are the ones who cannot run this suite to find out, so a Linux runner has to be able to drive both
|
|
718
|
+
* answers rather than read the source and agree with it.
|
|
719
|
+
*
|
|
720
|
+
* A READ THAT FAILS ANSWERS "NOT WSL", and that is the direction that changes nothing: it leaves the
|
|
721
|
+
* resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start
|
|
722
|
+
* refusing candidates under /mnt on an ordinary Linux box with an ordinary mount.
|
|
723
|
+
*/
|
|
724
|
+
export function isWsl({ env = process.env, procVersion = null } = {}) {
|
|
725
|
+
if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
|
|
726
|
+
if (String(env.WSL_INTEROP ?? "").trim()) return true;
|
|
727
|
+
const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
|
|
728
|
+
return /microsoft|wsl/i.test(v);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* What to say about candidates passed over because they sit on a Windows drive — or `null` when none
|
|
733
|
+
* were. Plural because a reader can have both a shim and a wrapper on PATH.
|
|
734
|
+
*/
|
|
735
|
+
export function windowsShimNote(skipped, bin) {
|
|
736
|
+
if (!skipped?.length) return null;
|
|
737
|
+
return `ignored ${skipped.length === 1 ? "a `" + bin + "` on a Windows drive" : skipped.length + " `" + bin + "` binaries on Windows drives"} `
|
|
738
|
+
+ `(${skipped.join(", ")}): WSL appends the Windows PATH to this one, and a Windows build cannot run a `
|
|
739
|
+
+ `stage here — it fails the proof turn as "not signed in" while holding a credential this side never `
|
|
740
|
+
+ `wrote. Install it inside the Linux distribution instead.`;
|
|
611
741
|
}
|
|
612
742
|
const isExec = (p) => { try { accessSync(p, constants.X_OK); return statSync(p).isFile(); } catch { return false; } };
|
|
613
743
|
|
|
744
|
+
/**
|
|
745
|
+
* What doctor says about the engine on a platform the run door refuses outright, or `null` where the
|
|
746
|
+
* ordinary binary checks apply.
|
|
747
|
+
*
|
|
748
|
+
* `platform` IS INJECTABLE, and for the reason `preflightEngineBinary` gives at its own refusal: the
|
|
749
|
+
* population this protects is the one that cannot run this suite to find out. An arm on a Linux runner
|
|
750
|
+
* has to be able to ask what a Windows reader is shown, or the Windows text is asserted by nobody —
|
|
751
|
+
* and reading `process.platform` inside the caller would make that arm read source instead of driving
|
|
752
|
+
* the answer.
|
|
753
|
+
*/
|
|
754
|
+
export function platformEngineRefusal({ platform = process.platform } = {}) {
|
|
755
|
+
if (platform !== "win32") return null;
|
|
756
|
+
return "this engine does not run on native Windows — stage subprocesses are spawned with POSIX path "
|
|
757
|
+
+ "and process semantics, so the run door refuses before it reads PATH. Run it under WSL2, or in "
|
|
758
|
+
+ "the devcontainer. The demo works here as it is: it replays finished reports and needs no engine.";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* How to leave demo mode, which is not the same instruction everywhere.
|
|
763
|
+
*
|
|
764
|
+
* On the platform the run door refuses, the standard advice is a loop: install a CLI the reader may
|
|
765
|
+
* already have, then restart a service — neither of which can change the answer, because the refusal
|
|
766
|
+
* is about the platform rather than the program. Naming WSL2 is the only instruction that ends it.
|
|
767
|
+
*/
|
|
768
|
+
export function leaveDemoAdvice(engSpec, { platform = process.platform } = {}) {
|
|
769
|
+
if (platform === "win32") {
|
|
770
|
+
return [`To leave demo on Windows: run the product under WSL2, or in the devcontainer. Installing `
|
|
771
|
+
+ `${engSpec.vendor}'s CLI natively will not change this — the run door refuses on the platform, `
|
|
772
|
+
+ "not on the program."];
|
|
773
|
+
}
|
|
774
|
+
return [
|
|
775
|
+
`To leave demo: install ${engSpec.vendor}'s CLI (\`${engSpec.fallback}\`)`
|
|
776
|
+
+ `${engSpec.install ? ` with \`${engSpec.install}\`` : ""}, then ${engSpec.signIn}.`,
|
|
777
|
+
"Restart any running engine service afterwards so it re-reads its PATH: the portal reports what the "
|
|
778
|
+
+ "engine saw when it last started, and it will not notice a new install until then.",
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
|
|
614
782
|
/**
|
|
615
783
|
* The engine menu, built from the driver's registry so the wizard cannot offer an adapter that does not
|
|
616
784
|
* exist — or hide one that does. Same guarantee the register-provider list has.
|
|
@@ -998,9 +1166,9 @@ export async function runCheck() {
|
|
|
998
1166
|
}
|
|
999
1167
|
|
|
1000
1168
|
say("\n Node");
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
else problem(`node ${
|
|
1169
|
+
const nodeV = nodeFloorVerdict();
|
|
1170
|
+
if (nodeV.ok) ok(`node ${nodeV.current}`);
|
|
1171
|
+
else problem(`node ${nodeV.current} — this engine needs ${nodeV.required} or newer; node:sqlite is not a built-in module before then`);
|
|
1004
1172
|
|
|
1005
1173
|
// Read the file up here rather than at the `.env` heading below: the engine section is the first that
|
|
1006
1174
|
// needs `effective()`, and which ENGINE is configured decides which binary variable to check. Reading
|
|
@@ -1123,7 +1291,28 @@ export async function runCheck() {
|
|
|
1123
1291
|
const binSet = !!binEff;
|
|
1124
1292
|
const binSetting = binEff?.v || engSpec.fallback;
|
|
1125
1293
|
const bin = resolveEngineBin(binSetting);
|
|
1126
|
-
|
|
1294
|
+
// ── NATIVE WINDOWS IS ANSWERED HERE, BEFORE ANY PATH IS RESOLVED OR REPORTED ──────────────────
|
|
1295
|
+
//
|
|
1296
|
+
// `resolveEngineBin` tests a candidate with `accessSync(X_OK)` and `isFile()`. Windows has no
|
|
1297
|
+
// execute bit, so X_OK is satisfied by any file that exists — and an npm global install writes
|
|
1298
|
+
// BOTH `claude`, an extensionless shell script for Git Bash, and `claude.cmd`. The POSIX test
|
|
1299
|
+
// passes on the shell script, so this line reported the engine FOUND at a path CreateProcess
|
|
1300
|
+
// cannot start, and the probe then failed with `spawn claude ENOENT`. Reported from a real
|
|
1301
|
+
// Windows run, 2026-09-09.
|
|
1302
|
+
//
|
|
1303
|
+
// RESOLVING `claude.cmd` INSTEAD WOULD MOVE THE CONTRADICTION, NOT REMOVE IT.
|
|
1304
|
+
// `preflightEngineBinary` refuses native Windows by name, and the run door calls it
|
|
1305
|
+
// unconditionally — so a clearance cannot run here whatever this line finds. Making the probe
|
|
1306
|
+
// succeed would produce a doctor that says ready, a probe that passes, and a run that refuses
|
|
1307
|
+
// anyway: the same disagreement one step later, and more convincing for having spawned something.
|
|
1308
|
+
//
|
|
1309
|
+
// So doctor says what the run door says. That is the property — the two agree — and the reader
|
|
1310
|
+
// gets one refusal they can act on instead of a found-then-failed sequence that sends them
|
|
1311
|
+
// looking for a PATH problem they do not have. The demo is unaffected: it replays finished runs
|
|
1312
|
+
// and needs no engine, which is why four reports published on that same Windows box.
|
|
1313
|
+
const platformRefusal = platformEngineRefusal();
|
|
1314
|
+
if (platformRefusal) problem(platformRefusal);
|
|
1315
|
+
else if (bin.executable && !bin.relative) ok(`${bin.path}`);
|
|
1127
1316
|
// The FACT only. It used to carry "install it for a real run (`npm run example` needs no engine)",
|
|
1128
1317
|
// which is the absence framing was filed about — and it now says half of what the MODE line
|
|
1129
1318
|
// below says, in worse words. One statement of a state, in the place that states states.
|
|
@@ -1134,6 +1323,15 @@ export async function runCheck() {
|
|
|
1134
1323
|
else if (bin.relative) problem(`${engSpec.env}="${binSetting}" is RELATIVE — stage subprocesses run with cwd set to the run directory, so it will not resolve there. Use an absolute path (${bin.path} from here)`);
|
|
1135
1324
|
else if (!bin.path) problem(`${engSpec.env}="${binSetting}" resolves to nothing on PATH`);
|
|
1136
1325
|
else problem(`${engSpec.env}="${binSetting}" → ${bin.path} is not an executable file`);
|
|
1326
|
+
// SAID AFTER THE CHAIN ABOVE, AND OUTSIDE IT. This block is one if/else-if ladder, so a statement
|
|
1327
|
+
// placed between two of its clauses re-parents every clause below onto the new `if` — measured:
|
|
1328
|
+
// it made doctor report an executable mock binary as "not an executable file", because the ladder's
|
|
1329
|
+
// tail became the else of THIS condition. Said whether or not a binary was found: under WSL a
|
|
1330
|
+
// Windows build on the appended PATH is passed over, and if a Linux one was found the reader still
|
|
1331
|
+
// needs to know which of the two they have, while if none was, "no binary" over a machine whose own
|
|
1332
|
+
// `which` prints one is a refusal nobody can act on.
|
|
1333
|
+
const shimNote = windowsShimNote(bin.skipped, engSpec.fallback);
|
|
1334
|
+
if (shimNote) info(shimNote);
|
|
1137
1335
|
|
|
1138
1336
|
// ── — WHICH MODE THIS INSTALL IS IN, said as a mode rather than as a list of absences ──────
|
|
1139
1337
|
//
|
|
@@ -1164,12 +1362,60 @@ export async function runCheck() {
|
|
|
1164
1362
|
if (installMode === ENGINE_MODES.DEMO) {
|
|
1165
1363
|
info("MODE: demo — everything works except starting a NEW search. The example report, its audit trail "
|
|
1166
1364
|
+ "and the MCP connection are live right now; `npm run example` needs no engine.");
|
|
1167
|
-
|
|
1365
|
+
// NAMES THE COMMAND, AND THE RESTART. The settings page says both now, and a doctor that named
|
|
1366
|
+
// the program but not how to install it — or that left out the restart, which is what actually
|
|
1367
|
+
// unsticks a reader who has just installed it — would be the third opinion this issue exists to
|
|
1368
|
+
// remove. Same words on all three surfaces, taken from the same table.
|
|
1369
|
+
// THE WAY OUT IS NOT THE SAME ON EVERY PLATFORM, and on the one where it differs the standard
|
|
1370
|
+
// advice is a loop: a Windows reader is told to install a CLI they may already have, and then to
|
|
1371
|
+
// restart a service — neither of which can change the answer, because the refusal is about the
|
|
1372
|
+
// platform rather than the binary. Naming WSL2 is the only instruction that ends this state.
|
|
1373
|
+
for (const line of leaveDemoAdvice(engSpec)) info(line);
|
|
1168
1374
|
} else {
|
|
1169
1375
|
info("The engine program is installed. Whether it is signed in cannot be read from disk: run "
|
|
1170
1376
|
+ "clearotron doctor --probe-engine to find out.");
|
|
1171
1377
|
}
|
|
1172
1378
|
|
|
1379
|
+
// AND WHETHER THE ENGINE AGREES, which is a different question from the one above and the reason an
|
|
1380
|
+
// outside user gave up on this product. This command reads its OWN environment; the portal's New
|
|
1381
|
+
// clearance screen reads what the engine recorded when it last started. Both were right and they
|
|
1382
|
+
// said opposite things, so a reader who checked the confident-looking one first was told the engine
|
|
1383
|
+
// was fine while no search would start. Reported here in the same words the configuration page uses,
|
|
1384
|
+
// because a doctor that cannot see a contradiction the product ships with is the wrong doctor.
|
|
1385
|
+
try {
|
|
1386
|
+
const { readFlagSnapshot, postureDisagreement } = await import("../driver/flag-snapshot.mjs");
|
|
1387
|
+
// THE READ-SIDE ACCESSOR, because this is a read: `config.poolRoot` throws on a box with no pool
|
|
1388
|
+
// configured, and `readFlagSnapshot(null)` already answers "no capture" for exactly that case.
|
|
1389
|
+
const snap = readFlagSnapshot(config.poolRootOrNull);
|
|
1390
|
+
const rows = snap
|
|
1391
|
+
? postureDisagreement(snap, { flags: {}, engine: engineInventory(invEnv) })
|
|
1392
|
+
: null;
|
|
1393
|
+
// NULL IS NOT AGREEMENT and neither is an empty pool — a box with no capture has nothing to
|
|
1394
|
+
// disagree with, and saying so beats printing a clean bill nobody measured.
|
|
1395
|
+
const clash = (rows ?? []).find((r) => r.what === "engine program");
|
|
1396
|
+
if (clash) {
|
|
1397
|
+
problem(`The engine that last ran and this machine disagree about the engine program: the last run `
|
|
1398
|
+
+ `recorded it as ${clash.capture}, this machine reads it as ${clash.live}. A NEW search will `
|
|
1399
|
+
+ `refuse while that is true. Restart the engine service so it re-reads its PATH, or install the `
|
|
1400
|
+
+ `CLI where the service can see it.`);
|
|
1401
|
+
}
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
// WHAT ACTUALLY REACHES THIS CATCH, established by driving it rather than by reading it.
|
|
1404
|
+
//
|
|
1405
|
+
// Not a box with no pool: `poolRootOrNull` answers null and `readFlagSnapshot(null)` answers null,
|
|
1406
|
+
// neither throwing. And NOT an unreadable capture either — `readFlagSnapshot` has its own try and
|
|
1407
|
+
// returns null for a corrupt file, so a damaged capture is already indistinguishable from an absent
|
|
1408
|
+
// one by the time this code sees it. That is worth knowing and is not this change's to fix.
|
|
1409
|
+
//
|
|
1410
|
+
// So this catch covers an import that has broken or an unexpected throw out of the comparison —
|
|
1411
|
+
// the check having stopped running. A doctor silent about its own failure is the defect the rest of
|
|
1412
|
+
// this change is about, an absence rendered as a clean bill, so it says so. A caution rather than a
|
|
1413
|
+
// problem: the engine may be perfectly fine and it is this check that is broken.
|
|
1414
|
+
info(`Could not compare this machine against what the engine last recorded (${e?.message ?? e}). `
|
|
1415
|
+
+ `That comparison is what catches a settings page reading healthy while a search will not start, `
|
|
1416
|
+
+ `so this run has not checked it either way.`);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1173
1419
|
// item 5 — WHICH BILLING LANE, reported rather than left to be inferred from a variable's
|
|
1174
1420
|
// absence. `--check` named the engine and its binary and never said how the box pays, so the two
|
|
1175
1421
|
// states that matter — metered per token, or drawn against a subscription — were indistinguishable
|
|
@@ -1366,7 +1612,7 @@ export async function runCheck() {
|
|
|
1366
1612
|
// Both lines were honest about their own source and neither said what it was. The first reads this
|
|
1367
1613
|
// command's env file; the second reads THIS PROCESS's resolution, and a CLI is started by a login
|
|
1368
1614
|
// shell that carries none of the units' `EnvironmentFile`. The deployment was correct and served
|
|
1369
|
-
//
|
|
1615
|
+
// the configured roster throughout.
|
|
1370
1616
|
//
|
|
1371
1617
|
// THE UNITS' ENV IS NOT PASSED INTO `profileStoreResolution`, deliberately. `PROFILES_OVERLAY_DIR`
|
|
1372
1618
|
// is captured when profiles.mjs LOADS, so handing it the units' value would set `live` with no
|
|
@@ -1418,17 +1664,38 @@ export async function runCheck() {
|
|
|
1418
1664
|
const demo = keys.filter((k) => roster.get(k)?.demoData === true);
|
|
1419
1665
|
// `generic` is the universal fallback the module requires by name, not a brand owner somebody
|
|
1420
1666
|
// onboarded — counting it would tell an operator with an empty store that they have one.
|
|
1421
|
-
|
|
1667
|
+
//
|
|
1668
|
+
// A DEMO ACCOUNT IS NOT AN ONBOARDED OWNER EITHER, and until this line it was counted as one. A
|
|
1669
|
+
// fresh install used to ship the demo account into every roster, so `doctor` reported "1 brand
|
|
1670
|
+
// owner(s) resolve here: demo-brand-owner (DEMO DATA)" on a machine where nobody had onboarded
|
|
1671
|
+
// anything — and never named `generic`, which is the account that actually rates a run there.
|
|
1672
|
+
// The reader is told they have a customer and not told what they are running on. Both halves
|
|
1673
|
+
// wrong from one list.
|
|
1674
|
+
//
|
|
1675
|
+
// SINCE 2026-09-08 A FRESH INSTALL RESOLVES `generic` ALONE (owner ruling): nobody should have to
|
|
1676
|
+
// clean demo material out of an environment they just created. So the demo branch below no longer
|
|
1677
|
+
// fires on a plain install — it fires inside the demo, which asks for its own account. It is kept
|
|
1678
|
+
// rather than deleted because it is still reachable, and a reader who meets the demo account
|
|
1679
|
+
// there is owed the same two facts: it is fiction, and a real clearance under it is refused at
|
|
1680
|
+
// the admission wall.
|
|
1681
|
+
//
|
|
1682
|
+
// Three states, told apart, because they mean three different things to whoever is reading:
|
|
1683
|
+
// an onboarded roster, the house default alone, and the house default beside what the demo
|
|
1684
|
+
// brought with it.
|
|
1685
|
+
const owners = keys.filter((k) => k !== "generic" && !demo.includes(k));
|
|
1422
1686
|
if (!owners.length) {
|
|
1423
|
-
|
|
1424
|
-
+
|
|
1687
|
+
const base = "`generic` is the account this install rates under — the house default, and the "
|
|
1688
|
+
+ "only one a clean install has";
|
|
1689
|
+
if (demo.length) {
|
|
1690
|
+
info(`${base}. The demo brought one with it, marked DEMO DATA: ${demo.join(", ")} — fiction `
|
|
1691
|
+
+ "rather than an account anybody onboarded, and a real clearance under one is refused");
|
|
1692
|
+
} else {
|
|
1693
|
+
info(`${base}. An empty store is a working install on Generic defaults; it is also what a `
|
|
1694
|
+
+ "store pointed at the wrong directory looks like");
|
|
1695
|
+
}
|
|
1425
1696
|
} else {
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
// The demo marker is the member-level half: naming the store is not the
|
|
1429
|
-
// same as saying the accounts in it are fiction, and a real clearance under one is refused at
|
|
1430
|
-
// the admission wall — which an operator should learn here rather than from that refusal.
|
|
1431
|
-
if (demo.length) info(`${line} — accounts marked DEMO DATA cannot start a real clearance`);
|
|
1697
|
+
const line = `${owners.length} brand owner(s) resolve here: ${owners.join(", ")}`;
|
|
1698
|
+
if (demo.length) info(`${line}. The demo brought one with it, marked DEMO DATA: ${demo.join(", ")} — not counted above, and a real clearance under one is refused`);
|
|
1432
1699
|
else ok(line);
|
|
1433
1700
|
}
|
|
1434
1701
|
try {
|
|
@@ -2118,7 +2385,7 @@ export async function runCheck() {
|
|
|
2118
2385
|
//
|
|
2119
2386
|
// `describeDoorState` was written for exactly this block and then never called from it: the split
|
|
2120
2387
|
// it encodes sat in `shared/client-door.mjs` with no caller in `bin/` or `driver/`, while doctor
|
|
2121
|
-
// went on printing the three sentences the split replaces. The defect
|
|
2388
|
+
// went on printing the three sentences the split replaces. The defect testing measured is one of
|
|
2122
2389
|
// them — a `connect` that died at `daemon-reload` had already written the fence and placed both
|
|
2123
2390
|
// units, and doctor said "the client door is on" over a unit that was inactive with nothing on its
|
|
2124
2391
|
// port. Every angle read as configured, because configured is all anything asked.
|
|
@@ -2310,6 +2577,27 @@ export async function runCheck() {
|
|
|
2310
2577
|
// unreachable; repeating it as a second failure teaches the reader that this section
|
|
2311
2578
|
// double-counts. It still states what was NOT established, which is the whole job.
|
|
2312
2579
|
else if (v.kind === "could-not-look") info(v.message);
|
|
2580
|
+
// NO ACCESS IN FRONT OF THE CLIENT DOOR IS A POSTURE, NOT A FAULT (owner ruling 2026-09-08).
|
|
2581
|
+
//
|
|
2582
|
+
// This is the CLIENT connector's address, and how a client reaches it is the client's decision:
|
|
2583
|
+
// "client access sitting behind OAuth is totally up to a client — plenty might just run it token
|
|
2584
|
+
// based on their own laptop." A door answering with its own Bearer challenge and no Access front
|
|
2585
|
+
// is a supported shape, not a misconfiguration, so raising it made `doctor` exit 1 on a healthy
|
|
2586
|
+
// deployment and taught its reader to skim the one command that must never be skimmed.
|
|
2587
|
+
//
|
|
2588
|
+
// It is still SAID rather than dropped, and it still states what was not established: an
|
|
2589
|
+
// audience that was never compared is not an audience that agreed. What changed is that the
|
|
2590
|
+
// sentence no longer calls a client's own arrangement a finding about this install.
|
|
2591
|
+
//
|
|
2592
|
+
// Scoped deliberately to `not-fronted` on THIS address. A configured audience that DISAGREES
|
|
2593
|
+
// with the one the edge issues is still a fault, and the portal and ops surfaces are untouched —
|
|
2594
|
+
// they are checked elsewhere and Access in front of them is not optional.
|
|
2595
|
+
else if (v.kind === "not-fronted") {
|
|
2596
|
+
info(`nothing is fronting this hostname with Access — ${read.why}. That is this client door's `
|
|
2597
|
+
+ "posture rather than a fault: a client may reach it with its own token and no Access in "
|
|
2598
|
+
+ "front, which is a supported shape. The configured audience was not compared against this "
|
|
2599
|
+
+ "address, so nothing here says the two agree.");
|
|
2600
|
+
}
|
|
2313
2601
|
else problem(v.message);
|
|
2314
2602
|
}
|
|
2315
2603
|
}
|
|
@@ -2534,13 +2822,13 @@ try {
|
|
|
2534
2822
|
|
|
2535
2823
|
// 1 ── Node
|
|
2536
2824
|
say(" Node");
|
|
2537
|
-
const
|
|
2538
|
-
if (
|
|
2539
|
-
problem(`node ${
|
|
2825
|
+
const nodeV = nodeFloorVerdict();
|
|
2826
|
+
if (!nodeV.ok) {
|
|
2827
|
+
problem(`node ${nodeV.current} — this engine needs ${nodeV.required} or newer. Upgrade Node and run setup again.`);
|
|
2540
2828
|
aborted = "node";
|
|
2541
2829
|
throw new Error("node floor");
|
|
2542
2830
|
}
|
|
2543
|
-
ok(`node ${
|
|
2831
|
+
ok(`node ${nodeV.current}`);
|
|
2544
2832
|
|
|
2545
2833
|
// 2 ── the engine: which one, which binary, and PROOF that it can run a turn
|
|
2546
2834
|
//
|
|
@@ -2570,7 +2858,32 @@ try {
|
|
|
2570
2858
|
if (!pick.id) { sayNoEngine(); break; }
|
|
2571
2859
|
const eng = ENGINE_BINARIES[pick.id];
|
|
2572
2860
|
|
|
2861
|
+
// ── NATIVE WINDOWS IS ANSWERED HERE TOO, AND THE ESCAPE IS OFFERED RATHER THAN LEFT ON A MENU ──
|
|
2862
|
+
//
|
|
2863
|
+
// Doctor learned this and the wizard did not, and the gap is worse than it sounds: the two "no
|
|
2864
|
+
// engine" escapes below both sit behind "no usable binary", and on Windows that is FALSE. X_OK is
|
|
2865
|
+
// satisfied by any file that exists, so the wizard says "found C:\...\claude", walks past both
|
|
2866
|
+
// escapes, asks which lane pays, spends a proof turn, and meets `spawn claude ENOENT`. The only
|
|
2867
|
+
// prompt left then defaults to No and returns to the engine menu — with nothing changed between
|
|
2868
|
+
// iterations, on either lane. Reported from a real Windows run: the menu's last row was the one
|
|
2869
|
+
// way out and nothing on screen said so.
|
|
2870
|
+
//
|
|
2871
|
+
// So the refusal is stated before a candidate is resolved, and finishing with no engine is OFFERED
|
|
2872
|
+
// rather than being a row the reader has to notice. Declining leaves them on the menu exactly as
|
|
2873
|
+
// before, which is the branch that was already there.
|
|
2874
|
+
const wizardRefusal = platformEngineRefusal();
|
|
2875
|
+
if (wizardRefusal) {
|
|
2876
|
+
problem(wizardRefusal);
|
|
2877
|
+
if (await confirm("Finish setup with no engine configured?", true)) { sayNoEngine(); break engine; }
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2573
2881
|
let bin = resolveEngineBin(process.env[eng.env] || eng.fallback);
|
|
2882
|
+
// Under WSL the Windows build on the appended PATH has been passed over. Said before the install
|
|
2883
|
+
// offer below, because otherwise a reader whose Linux install is genuinely missing is asked to
|
|
2884
|
+
// install a binary their own `which` already prints — and would decline for the wrong reason.
|
|
2885
|
+
const shimNote = windowsShimNote(bin.skipped, eng.fallback);
|
|
2886
|
+
if (shimNote) info(shimNote);
|
|
2574
2887
|
if (!(bin.executable && !bin.relative) && eng.install) {
|
|
2575
2888
|
// ── — INSTALLING IT IS ONE COMMAND, AND WE USED TO STOP AT A SENTENCE ───────────────────
|
|
2576
2889
|
//
|
|
@@ -2769,7 +3082,20 @@ try {
|
|
|
2769
3082
|
break engine;
|
|
2770
3083
|
}
|
|
2771
3084
|
problem(probeFailureText(v));
|
|
3085
|
+
// ── THE ENGINE'S OWN LAST LINES, UNDER EVERY VERDICT, ON BOTH LANES ──────────────────────────
|
|
3086
|
+
//
|
|
3087
|
+
// This used to be `if (v.detail)`, so a verdict carrying no detail printed a headline and nothing
|
|
3088
|
+
// else — and the reader had a classification with no evidence to check it against. That is the
|
|
3089
|
+
// worst shape for a heuristic: the signed-out test is a text match over the engine's output, it
|
|
3090
|
+
// matches broadly on purpose, and a reader shown only its conclusion cannot tell a genuinely
|
|
3091
|
+
// signed-out CLI from an unrelated failure whose words happened to match.
|
|
3092
|
+
//
|
|
3093
|
+
// SAY WHICH IT IS, INCLUDING WHEN THERE IS NOTHING. "The engine printed nothing" is itself the
|
|
3094
|
+
// diagnosis on a binary that died before it opened its mouth, and printing no line at all leaves
|
|
3095
|
+
// a reader unable to tell that from a wizard that decided not to show them.
|
|
2772
3096
|
if (v.detail) info(`engine said: ${v.detail}`);
|
|
3097
|
+
else info("engine said: nothing — it produced no output on either stream before it stopped.");
|
|
3098
|
+
if (v.basis) info(`(that reading is ${v.basis === "text-match" ? "matched out of the text above, not a signal from the provider" : v.basis})`);
|
|
2773
3099
|
// — THE HAND-OFF. Signing in is the one step of this sequence nobody here can perform for
|
|
2774
3100
|
// someone, so the wizard names the command, waits, and re-probes rather than ending at a
|
|
2775
3101
|
// description of what is wrong. The text comes from ENGINE_BINARIES so the two adapters cannot
|
|
@@ -3175,6 +3501,22 @@ try {
|
|
|
3175
3501
|
say(" fallback — falls through by name, so an empty store is a working install. Your own customers");
|
|
3176
3502
|
say(" are added here by name; the bundled demo customers never show through into your roster.");
|
|
3177
3503
|
|
|
3504
|
+
// 7c ── WHO SIGNS IN, AND WHAT THAT ADDRESS GRANTS
|
|
3505
|
+
//
|
|
3506
|
+
// Setup never asked for an address, so `clearotron start` derived one — and derived a staff-domain
|
|
3507
|
+
// rule from everything after its `@`. The whole reasoning, and the install that granted a
|
|
3508
|
+
// documentation domain to a reader who had never been asked, is on `askSignIn` above.
|
|
3509
|
+
section("Who signs in");
|
|
3510
|
+
const localAccount = (() => {
|
|
3511
|
+
try { return userInfo().username || "user"; } catch { return "user"; }
|
|
3512
|
+
})();
|
|
3513
|
+
// ONE CALL SITE. The loop itself lives in `askSignIn` so that the branch that matters — a reader who
|
|
3514
|
+
// pressed Enter at the grant question, and a staff rule that must therefore NOT be written — is
|
|
3515
|
+
// reachable without a terminal. The same seam and the same reason as `offerUsptoSync`.
|
|
3516
|
+
Object.assign(candidate, await askSignIn(
|
|
3517
|
+
{ askValue, confirm, say, ok, info, warn, problem },
|
|
3518
|
+
{ localAccount, staffLabel: `${BRAND.name} staff`, envPath: ENV_PATH }));
|
|
3519
|
+
|
|
3178
3520
|
// 8 ── the engine's own preflight over the whole candidate
|
|
3179
3521
|
//
|
|
3180
3522
|
// — THIS STEP CHECKS THE REGISTER PROVIDER AND NOTHING ELSE. `preflightCandidate` pins
|