@tokenoftrust/cli 1.4.1 → 1.5.0

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.
Files changed (53) hide show
  1. package/README.md +5 -0
  2. package/bin/tot.mjs +58 -79
  3. package/package.json +6 -1
  4. package/src/activity.mjs +15 -14
  5. package/src/app-scaffold.mjs +4 -4
  6. package/src/auth.mjs +13 -5
  7. package/src/candidate-state.mjs +3 -3
  8. package/src/commands/accept.mjs +65 -38
  9. package/src/commands/app/dev.mjs +8 -4
  10. package/src/commands/app/index.mjs +3 -3
  11. package/src/commands/app/scaffold.mjs +1 -1
  12. package/src/commands/branches.mjs +4 -3
  13. package/src/commands/cleanup.mjs +7 -11
  14. package/src/commands/clone.mjs +23 -20
  15. package/src/commands/dev.mjs +42 -24
  16. package/src/commands/doctor.mjs +4 -4
  17. package/src/commands/git-credential.mjs +2 -2
  18. package/src/commands/go-live.mjs +9 -5
  19. package/src/commands/grants.mjs +7 -5
  20. package/src/commands/hotfix.mjs +1 -1
  21. package/src/commands/ideas.mjs +2 -2
  22. package/src/commands/link.mjs +2 -2
  23. package/src/commands/login.mjs +5 -6
  24. package/src/commands/pr.mjs +33 -19
  25. package/src/commands/preview-build.mjs +6 -6
  26. package/src/commands/preview-doctor.mjs +225 -0
  27. package/src/commands/preview-retry-evidence.mjs +156 -0
  28. package/src/commands/preview.mjs +19 -3
  29. package/src/commands/revert.mjs +5 -5
  30. package/src/commands/rollback.mjs +18 -16
  31. package/src/commands/ship.mjs +29 -12
  32. package/src/commands/start.mjs +61 -51
  33. package/src/commands/submit.mjs +360 -50
  34. package/src/commands/sync.mjs +2 -2
  35. package/src/commands/validate.mjs +4 -3
  36. package/src/commands/whoami.mjs +1 -1
  37. package/src/dev-heartbeat.mjs +3 -2
  38. package/src/dev-logs.mjs +2 -2
  39. package/src/errors.mjs +11 -4
  40. package/src/git-credential.mjs +94 -21
  41. package/src/last-tenant.mjs +1 -1
  42. package/src/mcp.mjs +6 -1
  43. package/src/merge-doctor-report.mjs +208 -0
  44. package/src/oauth.mjs +18 -14
  45. package/src/obstacle-beacon.cjs +2 -2
  46. package/src/obstacle.mjs +1 -1
  47. package/src/plan.mjs +16 -21
  48. package/src/sample.mjs +4 -4
  49. package/src/validate.mjs +135 -15
  50. package/src/vendor/private-apps-devkit.mjs +3 -3
  51. package/src/viewer-session.mjs +118 -0
  52. package/template/private-app/README.md +12 -6
  53. package/src/commands/retire.mjs +0 -203
@@ -147,11 +147,11 @@ export async function run(argv, ctx) {
147
147
  }
148
148
 
149
149
  /**
150
- * Standalone-checkout entrypoint: native runtime by default (F3 — no Docker
150
+ * Standalone-checkout entrypoint: native runtime by default (no Docker
151
151
  * prerequisite), Docker on explicit request, and Docker as an automatic
152
152
  * fallback when the native artifact can't be fetched (e.g. the MCP's
153
153
  * dev_renderer_artifact isn't configured on this deployment yet) — so `tot dev`
154
- * keeps working through the WS3 rollout instead of hard-failing.
154
+ * keeps working instead of hard-failing.
155
155
  */
156
156
  async function runStandalone(workspace, args, ctx) {
157
157
  if (args.docker) return runContainer(workspace, args, ctx);
@@ -266,7 +266,7 @@ function runMonorepo(ctx, argv) {
266
266
  * Thrown when the native runtime can't be used for a reason that's about the
267
267
  * artifact/service, not the developer's checkout — runStandalone catches this
268
268
  * specifically and falls back to Docker instead of hard-failing. Exported so
269
- * `tot start` (C1/F3 integration) can catch it too and fall back the same way.
269
+ * `tot start` can catch it too and fall back the same way.
270
270
  */
271
271
  export class NativeArtifactUnavailableError extends Error {}
272
272
 
@@ -326,7 +326,7 @@ export function bootNative(runnerDir, workspace, port, url, args) {
326
326
  const handle = spawnNativeDev(runnerDir, workspace, port, { stdio: "piped", env: bridgeEnv });
327
327
  streamDevLogs(handle.child, { cockpitUrl: cockpitUrlFrom(bridgeEnv.TOT_DEV_ACTIVITY_URL) });
328
328
 
329
- // Heartbeat the hosted cockpit (G1) with the CLI version + this live localhost
329
+ // Heartbeat the hosted cockpit with the CLI version + this live localhost
330
330
  // URL while the runner runs — CLI-side, using the SAME bridge credential the
331
331
  // runner's file-save path uses (no-op in --sample, which threads no credential).
332
332
  // `cwd` (the checkout root) lets the cockpit show the FULL path to edit.
@@ -409,7 +409,7 @@ export function printSampleBanner({ url }, mode = "sample") {
409
409
  * Exported so sample/tests can drive the selection directly.
410
410
  * @param {ReturnType<typeof parseArgs>} args
411
411
  * @param {{ client?: any }} [opts] an already-authenticated MCP client (entitled path)
412
- * @returns {Promise<{kind:"public"|"entitled",version:string,url:string,strip:number,cacheKey:string}>}
412
+ * @returns {Promise<{kind:string,version:string,url:string,strip:number,cacheKey:string,integrity:string|null}>}
413
413
  */
414
414
  export async function resolveRendererSource(args, { client } = {}) {
415
415
  if (args.sample) return resolvePublicRendererSource(args);
@@ -577,6 +577,11 @@ function compareStableAsc(a, b) {
577
577
  * override with `--renderer-version` / TOT_RUNNER_VERSION. Package/registry
578
578
  * overridable via env for testing.
579
579
  */
580
+ /**
581
+ * @param {any} args
582
+ * @param {NodeJS.ProcessEnv} [env]
583
+ * @param {{ declaredVersion?: string|null }} [opts]
584
+ */
580
585
  export async function resolvePublicRendererSource(args, env = process.env, { declaredVersion = null } = {}) {
581
586
  const pkg = env.TOT_RUNNER_PACKAGE || PUBLIC_RUNNER_PACKAGE;
582
587
  const registry = (env.TOT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY).replace(/\/$/, "");
@@ -644,8 +649,9 @@ function isCompatibleMaintenancePatch(cliVersion, runnerVersion) {
644
649
  /**
645
650
  * ENTITLED source: the MCP's `dev_renderer_artifact` signed URL, gated on the
646
651
  * developer entitlement (same gate as the Docker pull token). Reuses an
647
- * already-authenticated `client` when provided (C1/F3), else establishes its own.
652
+ * already-authenticated `client` when provided, else establishes its own.
648
653
  */
654
+ /** @param {any} args @param {{ client?: any }} [opts] */
649
655
  export async function resolveEntitledRendererSource(args, { client: providedClient } = {}) {
650
656
  const baseUrl = args.mcp || process.env.MCP_BASE_URL || process.env.TOT_MCP_URL || DEFAULT_MCP_URL;
651
657
  const client = providedClient || createMcpClient(baseUrl);
@@ -680,11 +686,13 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
680
686
  * anything that should fall back to Docker (MCP unreachable, not entitled, not
681
687
  * configured, no corepack/pnpm on this host) rather than hard-failing `tot dev`.
682
688
  *
683
- * Exported + accepts an already-authenticated `client` (C1/F3 integration:
684
- * `tot start` reuses its own session and overlaps this with the checkout clone
689
+ * Exported + accepts an already-authenticated `client` (`tot start` reuses
690
+ * its own session and overlaps this with the checkout clone
685
691
  * instead of paying for a second client.initialize()+establishSession() — same
686
692
  * pattern as ensureRegistryLogin's `providedClient`). `tot dev` standalone
687
693
  * omits it and this establishes its own, as before.
694
+ * @param {any} args
695
+ * @param {{ client?: any }} [opts]
688
696
  * @returns {Promise<string>} the cached, installed runner tree's root directory.
689
697
  */
690
698
  export async function ensureRendererArtifact(args, { client: providedClient } = {}) {
@@ -865,11 +873,11 @@ export function probeRunnerVersion(runnerDir, { timeoutMs = 4000 } = {}) {
865
873
  * @returns {Promise<string>} the runner tree's root directory (has scripts/tot-dev.mjs).
866
874
  */
867
875
  export async function ensureSampleRenderer(args, ctx, { env = process.env, cacheRoot = RENDERER_CACHE_ROOT } = {}) {
868
- const src = resolveLocalRendererSource({
876
+ const src = /** @type {any} */ (resolveLocalRendererSource({
869
877
  env,
870
878
  mode: ctx?.mode,
871
879
  repoRoot: ctx?.repoRoot,
872
- });
880
+ }));
873
881
 
874
882
  if (src.kind === "dir") {
875
883
  if (!existsSync(join(src.dir, "scripts", "tot-dev.mjs"))) {
@@ -1063,7 +1071,7 @@ export async function installRunnerTarball(
1063
1071
  // of stopping a first run at an error only `rm -rf` folklore could clear.
1064
1072
  // Bounded to one retry so a genuinely-broken source still fails loudly.
1065
1073
  for (let attempt = 1; ; attempt++) {
1066
- const archivePath = isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource;
1074
+ const archivePath = /** @type {string} */ (isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource);
1067
1075
  const stagingDir = `${runnerDir}.staging-${process.pid}`;
1068
1076
  try {
1069
1077
  if (isUrl) {
@@ -1133,7 +1141,7 @@ export async function installRunnerTarball(
1133
1141
  exitCode: 2,
1134
1142
  },
1135
1143
  );
1136
- err.permanent = true;
1144
+ /** @type {any} */ (err).permanent = true;
1137
1145
  throw err;
1138
1146
  }
1139
1147
  writeCacheMarker(runnerDir, { version, integrity: contentId });
@@ -1285,7 +1293,7 @@ async function downloadFile(url, destPath) {
1285
1293
  if (!res.ok || !res.body) {
1286
1294
  throw new Error(`download failed: HTTP ${res.status} ${res.statusText}`);
1287
1295
  }
1288
- await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
1296
+ await pipeline(Readable.fromWeb(/** @type {any} */ (res.body)), createWriteStream(destPath));
1289
1297
  }
1290
1298
 
1291
1299
  /**
@@ -1334,7 +1342,8 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
1334
1342
  for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
1335
1343
  const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
1336
1344
  if (fd !== null && (r.error || r.status !== 0)) {
1337
- writeSync(fd, `[tot] corepack ${args.join(" ")} → ${r.error?.code || r.error?.message || `exit ${r.status}`}\n`);
1345
+ const rerr = /** @type {any} */ (r.error);
1346
+ writeSync(fd, `[tot] corepack ${args.join(" ")} → ${rerr?.code || rerr?.message || `exit ${r.status}`}\n`);
1338
1347
  }
1339
1348
  }
1340
1349
  } finally {
@@ -1378,6 +1387,10 @@ function spawnAsyncResult(cmd, args, opts = {}) {
1378
1387
  });
1379
1388
  }
1380
1389
 
1390
+ /**
1391
+ * @param {string} runnerDir
1392
+ * @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
1393
+ */
1381
1394
  export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
1382
1395
  const fd = logPath ? openSync(logPath, "a") : null;
1383
1396
  const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
@@ -1415,7 +1428,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1415
1428
  stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
1416
1429
  });
1417
1430
  if (r.status === 0) return; // installed
1418
- if (r.error?.code === "ENOENT") {
1431
+ if (/** @type {any} */ (r.error)?.code === "ENOENT") {
1419
1432
  // This launcher isn't on the machine — record it and try the next one.
1420
1433
  if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
1421
1434
  continue;
@@ -1446,7 +1459,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1446
1459
  (logPath ? `\n details: ${logPath}` : ""),
1447
1460
  { next: "install pnpm with `npm i -g pnpm` (or `corepack enable`), then re-run `tot start`" },
1448
1461
  );
1449
- err.permanent = true;
1462
+ /** @type {any} */ (err).permanent = true;
1450
1463
  throw err;
1451
1464
  } finally {
1452
1465
  if (fd !== null) closeSync(fd);
@@ -1501,6 +1514,10 @@ function missingPackage404(logPath) {
1501
1514
  * fs-watch HMR. Returns a handle shaped like spawnDevContainer's, so run()'s
1502
1515
  * auto-open-browser logic works unchanged for either runtime. Exported (and
1503
1516
  * `stdio` overridable) so `tot start` can pipe the logs instead of inheriting.
1517
+ * @param {string} runnerDir
1518
+ * @param {string} workspace
1519
+ * @param {string} port
1520
+ * @param {{ stdio?: any, env?: Record<string, any> }} [opts]
1504
1521
  */
1505
1522
  export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit", env = {} } = {}) {
1506
1523
  const script = join(runnerDir, "scripts", "tot-dev.mjs");
@@ -1512,7 +1529,7 @@ export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit",
1512
1529
  // `env` (e.g. activityBridgeEnv()) merges OVER process.env — {} is a pure
1513
1530
  // passthrough, identical to the old no-env-key behavior. Append `--no-warnings`
1514
1531
  // to NODE_OPTIONS so the runner's Node process never emits the scary
1515
- // `(node:NNNN) ExperimentalWarning: …` boot noise to the developer (b4);
1532
+ // `(node:NNNN) ExperimentalWarning: …` boot noise to the developer;
1516
1533
  // appended (not replaced) so any host-set NODE_OPTIONS is preserved.
1517
1534
  const mergedEnv = { ...process.env, ...env };
1518
1535
  mergedEnv.NODE_OPTIONS = `${mergedEnv.NODE_OPTIONS ? `${mergedEnv.NODE_OPTIONS} ` : ""}--no-warnings`;
@@ -1521,7 +1538,7 @@ export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit",
1521
1538
  [script, "--workspace", workspace, "--port", port],
1522
1539
  { cwd: runnerDir, stdio: stdioArr, env: mergedEnv },
1523
1540
  );
1524
- const handle = { child, exited: false, done: null };
1541
+ const handle = { child, exited: false, done: /** @type {any} */ (null) };
1525
1542
  handle.done = new Promise((resolvePromise) => {
1526
1543
  child.on("exit", (code) => {
1527
1544
  handle.exited = true;
@@ -1555,7 +1572,7 @@ async function runContainer(workspace, args, ctx) {
1555
1572
  const handle = await spawnDevContainer(plan, args, { stdio: "piped" });
1556
1573
  streamDevLogs(handle.child, { cockpitUrl: cockpitUrlFrom(activityBridgeEnv().TOT_DEV_ACTIVITY_URL) });
1557
1574
 
1558
- // Heartbeat the hosted cockpit (G1) CLI-side while the container runs — the
1575
+ // Heartbeat the hosted cockpit CLI-side while the container runs — the
1559
1576
  // container reports file-saves via the threaded env, but the CLI owns the
1560
1577
  // version + live URL. Same bridge credential (activityBridgeEnv), no-op absent.
1561
1578
  const stopHeartbeat = startHeartbeatFromEnv(activityBridgeEnv(), { url: plan.url, cwd: plan.workspace });
@@ -1580,7 +1597,7 @@ async function runContainer(workspace, args, ctx) {
1580
1597
 
1581
1598
  /**
1582
1599
  * Resolve the runner image ref the same way buildContainerPlan does — exported
1583
- * so a caller (`tot start`, for C1) can know the image, and therefore prefetch
1600
+ * so a caller (`tot start`) can know the image, and therefore prefetch
1584
1601
  * a registry login (below), before the checkout dir exists.
1585
1602
  */
1586
1603
  export function resolveDevImage(args, env = process.env) {
@@ -1676,9 +1693,9 @@ export async function spawnDevContainer(plan, args, { stdio = "inherit" } = {})
1676
1693
 
1677
1694
  const stdioArr =
1678
1695
  stdio === "piped" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
1679
- const child = spawn("docker", plan.dockerArgs, { stdio: stdioArr });
1696
+ const child = spawn("docker", plan.dockerArgs, { stdio: /** @type {any} */ (stdioArr) });
1680
1697
 
1681
- const handle = { child, exited: false, done: null };
1698
+ const handle = { child, exited: false, done: /** @type {any} */ (null) };
1682
1699
  handle.done = new Promise((resolvePromise) => {
1683
1700
  child.on("exit", (code) => {
1684
1701
  handle.exited = true;
@@ -1714,10 +1731,11 @@ export function isPrivateRegistryImage(image) {
1714
1731
  * the session, or the tool isn't available yet, warn softly and continue (an
1715
1732
  * existing `docker login`, or the docker pull error, takes over).
1716
1733
  *
1717
- * Exported + accepts an already-authenticated `client` (C1: `tot start` reuses
1734
+ * Exported + accepts an already-authenticated `client` (`tot start` reuses
1718
1735
  * its own session and overlaps this with the checkout clone instead of paying
1719
1736
  * for a second client.initialize()+establishSession() serially afterward).
1720
1737
  * `tot dev` standalone omits it and this establishes its own, as before.
1738
+ * @param {string} image @param {any} args @param {{ client?: any }} [opts]
1721
1739
  */
1722
1740
  export async function ensureRegistryLogin(image, args, { client: providedClient } = {}) {
1723
1741
  const registry = String(image).split("/")[0];
@@ -1767,7 +1785,7 @@ export function dockerAvailable() {
1767
1785
  * thing `open -a` can launch unattended; other platforms report false and
1768
1786
  * leave it to the caller's usual next-command message). Returns true once
1769
1787
  * Docker answers, false if it doesn't within the wait window. Shared by
1770
- * `tot start`'s preflight and `tot doctor --fix` (F2).
1788
+ * `tot start`'s preflight and `tot doctor --fix`.
1771
1789
  */
1772
1790
  export async function tryStartDocker() {
1773
1791
  if (process.platform !== "darwin") return false;
@@ -6,12 +6,12 @@
6
6
  * auth identity is available, and Docker — plus reports the detected context
7
7
  * so a developer knows which mode `tot` will use here.
8
8
  *
9
- * Docker is informational-only (not blocking): WS3/F3 made the native runtime
9
+ * Docker is informational-only (not blocking): the native runtime is
10
10
  * the default for `tot dev`/`tot start`, so a Docker that isn't running no
11
11
  * longer stops the loop — it only matters if you pass --docker or the native
12
12
  * artifact can't be fetched (see dev.mjs#NativeArtifactUnavailableError).
13
13
  *
14
- * `--fix` (F2) auto-remediates what it safely can, each fix announced as it
14
+ * `--fix` auto-remediates what it safely can, each fix announced as it
15
15
  * runs: starts Docker (macOS, opportunistically — harmless even though it's
16
16
  * no longer required), creates ~/.tot if it's missing, and signs you in when
17
17
  * there's no usable session at all. Anything it can't fix (or won't — a stale
@@ -135,9 +135,9 @@ export function collectChecks(_ctx, env = process.env) {
135
135
 
136
136
  /**
137
137
  * Auto-remediate the checks that are safely fixable without human judgment
138
- * (F2) — announcing each one as it runs. A stale-but-refreshable sign-in isn't
138
+ * — announcing each one as it runs. A stale-but-refreshable sign-in isn't
139
139
  * handled here because it isn't a failing check: the developer session refreshes
140
- * it silently the next time it's actually used (B4).
140
+ * it silently the next time it's actually used.
141
141
  */
142
142
  async function applyFixes(checks, env) {
143
143
  const totDir = join(env.TOT_HOME || homedir(), ".tot");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `tot git-credential` — git's own credential-helper protocol (see `git help
3
3
  * gitcredentials`), implemented over the developer's cached `tot login`
4
- * session (unit u10, workstream tot-merge-conflict-resolution-ux). `tot
4
+ * session. `tot
5
5
  * clone` configures a fresh checkout's `credential.helper` to run this (see
6
6
  * ../git-credential.mjs's CREDENTIAL_HELPER), so `git fetch`/`git push`/`git
7
7
  * pull` — run DIRECTLY by the developer, not just through `tot preview` —
@@ -93,7 +93,7 @@ export function readStdin() {
93
93
  * globally-scoped helper (or a checkout with a submodule / unrelated remote)
94
94
  * must not become a way to exfiltrate the token to an arbitrary host.
95
95
  * @param {{
96
- * env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string,
96
+ * env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string|null,
97
97
  * createClient?: typeof createMcpClient,
98
98
  * establish?: typeof establishSession,
99
99
  * checkout?: typeof checkoutTenant,
@@ -1,10 +1,10 @@
1
1
  /**
2
- * `tot go-live` — cut the apex domain over to the storefront (unit u9). The CLI
2
+ * `tot go-live` — cut the apex domain over to the storefront. The CLI
3
3
  * counterpart to the admin Publish tab's Domain "Connect" button: it drives the
4
4
  * SAME go-live CI/dispatch the button does (the storefront `/api/domain/dispatch`
5
5
  * → `www-domain` repository_dispatch → `cutover-dns.mjs`), gated by the SAME
6
6
  * server-authoritative readiness gate the admin display reads
7
- * (`GET /api/domain/readiness`, unit u9). This command CONSUMES those seams — it
7
+ * (`GET /api/domain/readiness`). This command CONSUMES those seams — it
8
8
  * does NOT reimplement the DNS cutover, the readiness policy, or the revert.
9
9
  *
10
10
  * tot go-live show apex readiness, then (if ready) connect the apex
@@ -19,7 +19,7 @@
19
19
  * revert). Fail-closed: a CONNECT is refused unless the server reports
20
20
  * `ready:true` — the CLI never recomputes readiness, it reads the same gate
21
21
  * the cutover is governed by, so they cannot diverge.
22
- * 2. Owner-only: the server resolves owner capability (u10); a non-owner is
22
+ * 2. Owner-only: the server resolves owner capability; a non-owner is
23
23
  * DENIED with the itemized reason. The CLI does not assert ownership itself.
24
24
  * 3. ALWAYS require ONE explicit [y/N] confirm (default NO). There is NO
25
25
  * `--yes`/`--force`; in a NON-TTY (CI, piped) it REFUSES rather than
@@ -196,6 +196,9 @@ export function renderReadiness({ appDomain, readiness, action }) {
196
196
  const lines = ["", ` Apex ${action === "rollback" ? "rollback" : "cutover"} readiness for ${appDomain ?? "this store"}:`];
197
197
  for (const c of readiness.checks) {
198
198
  lines.push(` ${c.ok ? "✓" : "✗"} ${c.label}${c.detail ? ` — ${c.detail}` : ""}`);
199
+ // Blocked checks carry a "how to clear this" remedy from the server gate —
200
+ // the same guidance the admin panel shows, so the two surfaces never diverge.
201
+ if (!c.ok && c.remedy) lines.push(` → ${c.remedy}`);
199
202
  }
200
203
  if (action === "connect") {
201
204
  lines.push(
@@ -217,6 +220,7 @@ export function renderReadiness({ appDomain, readiness, action }) {
217
220
  * assuming it from the dispatch. Injectable delay/attempts. Returns the last
218
221
  * normalised run status (may still be "dispatched" if CI is slow — reported honestly).
219
222
  * @param {{ get:(path:string)=>Promise<any> }} http
223
+ * @param {{ attempts?: number, delayMs?: number, sleep?: Function }} [opts]
220
224
  */
221
225
  export async function pollRunTerminal(http, { attempts = 8, delayMs = 2000, sleep } = {}) {
222
226
  const wait = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -391,7 +395,7 @@ function cap(s) {
391
395
  * ship floor. Throws on a non-2xx with the server's error message (so the caller
392
396
  * surfaces the real refusal). `fetchImpl` is injectable for tests.
393
397
  * @param {string} base
394
- * @param {{ token:string|undefined, owner:string, capability?:string, fetchImpl?:typeof fetch }} auth
398
+ * @param {{ token?:string, owner?:string, capability?:string, fetchImpl?:typeof fetch }} [auth]
395
399
  */
396
400
  export function createStorefrontHttp(base, { token, owner, capability = "ship-on-behalf", fetchImpl } = {}) {
397
401
  const root = base.replace(/\/+$/, "");
@@ -476,7 +480,7 @@ export async function run(argv, ctx) {
476
480
  const http = createStorefrontHttp(base, { token, owner });
477
481
  return await runGoLive(
478
482
  http,
479
- { appDomain: owner, action: args.action, rehearsal: args.rehearsal, noOpen: args.noOpen },
483
+ { appDomain: owner, action: /** @type {any} */ (args.action), rehearsal: args.rehearsal, noOpen: args.noOpen },
480
484
  { openUrl: (u) => openBrowser(u) },
481
485
  );
482
486
  }
@@ -61,9 +61,10 @@ function parseArgs(argv) {
61
61
  * exp: number|null, revokedAt: string|null }> }}
62
62
  */
63
63
  export function normalizeGrantRows(resp) {
64
- if (resp && typeof resp === "object" && !Array.isArray(resp) && Array.isArray(resp.grants)) {
65
- const activeTenant = typeof resp.activeTenant === "string" ? resp.activeTenant : null;
66
- const rows = resp.grants
64
+ const rr = /** @type {any} */ (resp);
65
+ if (resp && typeof resp === "object" && !Array.isArray(resp) && Array.isArray(rr.grants)) {
66
+ const activeTenant = typeof rr.activeTenant === "string" ? rr.activeTenant : null;
67
+ const rows = rr.grants
67
68
  .map((g) => {
68
69
  const tenant = g?.tenant ?? g?.id ?? null;
69
70
  return {
@@ -83,7 +84,7 @@ export function normalizeGrantRows(resp) {
83
84
 
84
85
  // Fallback: client_list — store ids + environment + the selected marker only. No
85
86
  // grant detail is knowable here (grantActive/capability/tier/exp/revokedAt stay null).
86
- const clients = Array.isArray(resp) ? resp : resp?.clients || resp?.tenants || [];
87
+ const clients = Array.isArray(resp) ? resp : rr?.clients || rr?.tenants || [];
87
88
  const rows = (Array.isArray(clients) ? clients : [])
88
89
  .map((c) => ({
89
90
  tenant: c?.tenant ?? c?.id ?? c?.clientId ?? c?.appDomain ?? null,
@@ -147,6 +148,7 @@ export function formatExpiry(exp, { now = Date.now() } = {}) {
147
148
  export function describeGrantRow(row, { now = Date.now() } = {}) {
148
149
  const status = grantStatus(row, { now });
149
150
  if (status === "unknown") return "grant detail unavailable (server introspection not enabled)";
151
+ /** @type {string[]} */
150
152
  const parts = [status];
151
153
  if (row.capability) parts.push(`capability=${row.capability}`);
152
154
  if (row.tier) parts.push(`tier=${row.tier}`);
@@ -238,7 +240,7 @@ export async function run(argv, _ctx) {
238
240
  return 1;
239
241
  }
240
242
  if (rows.length === 0) {
241
- // Status-aware, like whoami/checkout (card c2): an UNLINKED identity is told to
243
+ // Status-aware, like whoami/checkout: an UNLINKED identity is told to
242
244
  // link, not given the "may still be propagating" copy — that's only the genuine
243
245
  // linked-but-zero-grants case.
244
246
  const g = noStoresGuidance(resp);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `tot hotfix` (unit b22) — the OWNER-ONLY EXCEPTION LANE: release an urgent
2
+ * `tot hotfix` — the OWNER-ONLY EXCEPTION LANE: release an urgent
3
3
  * production fix from `main` to live WHILE `preview` still contains other unshipped
4
4
  * work, EXCLUDING that unshipped preview head, then automatically forward-integrate
5
5
  * `main` into `preview` and re-validate.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `tot ideas` — a gallery of copy-paste reskin prompts that reliably wow (G3).
2
+ * `tot ideas` — a gallery of copy-paste reskin prompts that reliably wow.
3
3
  * Not a script to run — inspiration for the AI moment: paste one into Claude
4
4
  * (after `tot start`'s "Connect Claude?" prompt, or `claude mcp add` directly)
5
5
  * and watch your store change and reload.
6
6
  *
7
7
  * IDEAS[0] doubles as the seeded first prompt `tot start` ends on and drops
8
- * you into (G2, see commands/start.mjs) — one list, no drift between the two.
8
+ * you into (see commands/start.mjs) — one list, no drift between the two.
9
9
  */
10
10
 
11
11
  /** Reskin prompts, ordered strongest-first. Pure data — exported for reuse + tests. */
@@ -54,7 +54,7 @@ Run this when \`tot whoami\` / \`tot start\` say your identity isn't linked yet.
54
54
  * @returns {{ authUrl: string|null, pollHandle: string|null }}
55
55
  */
56
56
  export function linkBeginFields(res) {
57
- const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
57
+ const c = /** @type {any} */ (res && typeof res === "object" && !Array.isArray(res) ? res : {});
58
58
  const authUrl =
59
59
  c.authUrl ||
60
60
  c.url ||
@@ -77,7 +77,7 @@ export function linkBeginFields(res) {
77
77
  * @returns {"linked"|"pending"|string}
78
78
  */
79
79
  export function linkPollStatus(res) {
80
- const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
80
+ const c = /** @type {any} */ (res && typeof res === "object" && !Array.isArray(res) ? res : {});
81
81
  if (c.linked === true || c.done === true || c.complete === true) return "linked";
82
82
  const raw =
83
83
  (typeof c.status === "string" && c.status) || (typeof c.state === "string" && c.state) || "";
@@ -11,7 +11,7 @@
11
11
  * TOT_MCP_URL, else the production MCP) so the cached token matches where it's
12
12
  * spent — point both at qa with `--mcp https://mcp.qa.tokenoftrust.com` or the env.
13
13
  *
14
- * B3 — `--device` forces the RFC 8628 device-code flow (headless/SSH/no-browser:
14
+ * `--device` forces the RFC 8628 device-code flow (headless/SSH/no-browser:
15
15
  * print a code, poll until it's approved elsewhere). It's also the automatic
16
16
  * fallback for a bare `tot login` when there's no browser opener on this box at
17
17
  * all (oauth.mjs's loopback flow would otherwise hang forever waiting for a
@@ -116,11 +116,11 @@ After signing in, run \`tot whoami\` to confirm, then \`tot clone\` / \`tot subm
116
116
  * device-code flow if `device` is set or the loopback can't be reached at
117
117
  * all) and cache the result, reusing a previously-registered client for THIS
118
118
  * MCP so we don't re-register (and burn the DCR rate limit) on every login.
119
- * Exported so `tot doctor --fix` (F2) can re-login as an auto-remediation
119
+ * Exported so `tot doctor --fix` can re-login as an auto-remediation
120
120
  * without duplicating this.
121
121
  * @returns {Promise<object>} the credentials written to disk.
122
122
  */
123
- export async function loginAndCache(mcpUrl, env = process.env, { log = () => {}, device = false } = {}) {
123
+ export async function loginAndCache(mcpUrl, env = process.env, { log = /** @type {(m?: string) => void} */ (() => {}), device = false } = {}) {
124
124
  const path = defaultCredentialsPath(env);
125
125
  const prior = readCredentials(path);
126
126
  const clientId = prior && prior.mcpUrl === mcpUrl ? prior.clientId : undefined;
@@ -196,11 +196,10 @@ export async function offerSignIn(mcpUrl, env = process.env, {
196
196
  * `log` carries the fingerprint + "waiting for approval" lines to the terminal.
197
197
  * @returns {Promise<object>} the credentials written to disk.
198
198
  */
199
- export async function redeemAndCache(mcpUrl, code, env = process.env, { log = () => {} } = {}) {
199
+ export async function redeemAndCache(mcpUrl, code, env = process.env, { log = /** @type {(m?: string) => void} */ (() => {}) } = {}) {
200
200
  const path = defaultCredentialsPath(env);
201
201
  const prior = readCredentials(path);
202
- const clientId = prior && prior.mcpUrl === mcpUrl ? prior.clientId : undefined;
203
- const creds = await rendezvousLoginFlow({ mcpUrl, clientId, code, log });
202
+ const creds = await rendezvousLoginFlow({ mcpUrl, code, log });
204
203
  const merged = mergeActivityBridge(prior, mcpUrl, creds);
205
204
  writeCredentials(path, merged);
206
205
  return merged;
@@ -7,7 +7,7 @@
7
7
  * tot pr close <N|id> close (reject) a candidate without merging
8
8
  *
9
9
  * A candidate PR is the reviewable unit `tot submit` creates. By default a
10
- * re-submit UPDATES your open candidate; `tot submit --new` forks another. This
10
+ * re-submit UPDATES your open candidate; `tot submit --fork-candidate` forks another. This
11
11
  * command fills the gap the raw submit loop left — a first-party way to list your
12
12
  * open candidates and to close one (candidate close was otherwise gated to
13
13
  * version-control apps only).
@@ -17,9 +17,9 @@
17
17
  * `candidate_status` / `candidate_close` tools. Dependency-free (global fetch +
18
18
  * `git`).
19
19
  *
20
- * OPERATOR MODE (unit U17). `tot pr list --tenant <t>` lists a tenant's OPEN
20
+ * OPERATOR MODE. `tot pr list --tenant <t>` lists a tenant's OPEN
21
21
  * candidate queue WITHOUT a checkout — the read-only companion to `tot ship --pr <N>
22
- * --tenant <t>` (U16). It reuses U16's exact transport: the SAME `GET /api/changes`
22
+ * --tenant <t>`. It reuses that command's exact transport: the SAME `GET /api/changes`
23
23
  * HTTP endpoint + operator-secret Bearer auth (`resolveOperatorSecret`,
24
24
  * `normalizeChangesQueue` from ship.mjs). `view`/`close` stay developer-only.
25
25
  */
@@ -28,9 +28,8 @@ import { createMcpClient } from "../mcp.mjs";
28
28
  import { establishSession, AuthUnavailableError } from "../auth.mjs";
29
29
  import { fail } from "../errors.mjs";
30
30
  import { repoNameFromRemote, currentBranch } from "./submit.mjs";
31
- // U17 reuses U16's operator-secret transport helpers verbatim (same env precedence,
32
- // same queue normalisation) so the operator `tot pr list` and `tot ship --pr` speak
33
- // one wire, not two.
31
+ // `tot pr list` reuses `tot ship --pr`'s operator-secret transport helpers verbatim
32
+ // (same env precedence, same queue normalisation) so the two speak one wire, not two.
34
33
  import { resolveOperatorSecret, normalizeChangesQueue } from "./ship.mjs";
35
34
  import {
36
35
  defaultCandidateStatePath,
@@ -71,7 +70,7 @@ const USAGE = `tot pr — see and manage candidate PRs
71
70
 
72
71
  DEVELOPER (default) — run inside your OWN store checkout: lists/manages the
73
72
  candidates \`tot submit\` opens. A re-submit updates your open one by default;
74
- \`tot submit --new\` forks another.
73
+ \`tot submit --fork-candidate\` forks another.
75
74
 
76
75
  OPERATOR — \`tot pr list --tenant <appDomain>\` lists ANY tenant's open candidate
77
76
  queue WITHOUT a checkout (the read-only companion to \`tot ship --pr <N> --tenant\`).
@@ -135,8 +134,8 @@ export function matchCandidate(candidates, target) {
135
134
 
136
135
  /**
137
136
  * One-line candidate summary for `tot pr list` — surfaces branch ↔ PR# ↔ preview
138
- * URL so a dev sees, at a glance, which git branch each candidate belongs to (u4 —
139
- * branch-bound candidates) and where its preview lives. ONLY `previewUrl` (the
137
+ * URL so a dev sees, at a glance, which git branch each candidate belongs to
138
+ * (branch-bound candidates) and where its preview lives. ONLY `previewUrl` (the
140
139
  * storefront-owned `/preview/<tenant>/pr/<N>` link) — NEVER `url` (the forge/
141
140
  * Gitea `html_url`), which must never reach a terminal (2026-08-18 incident:
142
141
  * a raw forge PR URL reached an owner). `active` marks the one
@@ -153,10 +152,10 @@ export function formatCandidateLine(c, { active = false } = {}) {
153
152
  return ` PR ${pr} ${branch} ${c.changeId} [${c.state ?? "?"}]${urlPart}${activePart}`;
154
153
  }
155
154
 
156
- // ─── Operator queue listing (unit U17) ──────────────────────────────────────────────
155
+ // ─── Operator queue listing ──────────────────────────────────────────────────────────
157
156
  //
158
- // The read-only companion to `tot ship --pr <N> --tenant <t>` (U16): list a tenant's
159
- // OPEN candidate queue WITHOUT a checkout. It reuses U16's exact transport — the SAME
157
+ // The read-only companion to `tot ship --pr <N> --tenant <t>`: list a tenant's
158
+ // OPEN candidate queue WITHOUT a checkout. It reuses that command's exact transport — the SAME
160
159
  // `GET /api/changes` endpoint (operator accept queue) + Bearer operator-secret auth
161
160
  // (`resolveOperatorSecret` / `normalizeChangesQueue` from ship.mjs) — so a listing and
162
161
  // a ship read one wire. NO merge/deploy/side-effect; pure listing.
@@ -172,8 +171,15 @@ export function formatCandidateLine(c, { active = false } = {}) {
172
171
  * sees it's listable but must `tot preview build` before it can ship.
173
172
  * Adapts `formatCandidateLine`'s house style to the `GET /api/changes` shape (`status`
174
173
  * not `state`; `previewUrl`; a `headSha` to short-render). Pure — unit-tested.
174
+ *
175
+ * A built row whose `evidence.pushFailed` is set (the MCP `change_report_evidence`
176
+ * push never landed — fb-1787938036559-ppmu4e) gets a `[evidence push failed]` tag
177
+ * + a `tot preview retry-evidence` hint, so the gap between "storefront thinks this
178
+ * is promotable" and "MCP has no evidence for it" is visible right where an
179
+ * operator is already looking, not just in the activity log.
175
180
  * @param {{prNumber?:number|null, status?:string|null, changeId?:string|null,
176
- * headSha?:string|null, previewUrl?:string|null, built?:boolean}} c
181
+ * headSha?:string|null, previewUrl?:string|null, built?:boolean,
182
+ * evidence?:{pushFailed?:boolean}|null}} c
177
183
  * @returns {string}
178
184
  */
179
185
  export function formatOperatorCandidateLine(c) {
@@ -190,6 +196,13 @@ export function formatOperatorCandidateLine(c) {
190
196
  }
191
197
  const status = c.status ?? "?";
192
198
  const urlPart = c.previewUrl ? ` ${c.previewUrl}` : "";
199
+ if (c.evidence?.pushFailed) {
200
+ const retryHint =
201
+ typeof c.prNumber === "number"
202
+ ? ` → tot preview retry-evidence --pr ${c.prNumber}`
203
+ : " → tot preview retry-evidence";
204
+ return ` PR ${pr} [${status}] [evidence push failed] ${c.changeId} ${head}${urlPart}${retryHint}`;
205
+ }
193
206
  return ` PR ${pr} [${status}] ${c.changeId} ${head}${urlPart}`;
194
207
  }
195
208
 
@@ -210,7 +223,7 @@ export function sortQueueByPrDesc(changes) {
210
223
  /**
211
224
  * The OPERATOR `tot pr list --tenant <t>` flow: fetch the tenant's OPEN candidate
212
225
  * queue over `GET /api/changes` (Bearer operator secret + `X-Tot-Owner` +
213
- * `x-tot-capability: ship-on-behalf`, mirroring U16) and print each candidate one per
226
+ * `x-tot-capability: ship-on-behalf`) and print each candidate one per
214
227
  * line, sorted by PR number desc. Fail-closed: no secret → honest refusal (exit 2)
215
228
  * BEFORE any network. `fetch` is injected so it's unit-tested with no live network.
216
229
  *
@@ -225,8 +238,9 @@ export async function runPrListOperator({ tenant, secret, storefrontUrl = null }
225
238
  if (!secret) {
226
239
  console.error(
227
240
  fail(
228
- "listing a tenant's queue is an OPERATOR action — it needs an operator secret",
229
- "set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret",
241
+ "no operator secret — view this tenant's queue from the storefront admin instead.",
242
+ `open https://${tenant}/admin → Publish tab to see the candidate queue and act on it. ` +
243
+ "Operators/CI: set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret.",
230
244
  ),
231
245
  );
232
246
  return 2;
@@ -287,12 +301,12 @@ export async function run(argv, ctx) {
287
301
  console.log(USAGE);
288
302
  return 0;
289
303
  }
290
- if (!SUBCOMMANDS.includes(args.sub)) {
304
+ if (!SUBCOMMANDS.includes(/** @type {any} */ (args.sub))) {
291
305
  console.error(fail(`unknown subcommand: \`tot pr ${args.sub}\``, "tot pr list | view <N> | close <N>"));
292
306
  return 2;
293
307
  }
294
308
 
295
- // OPERATOR MODE (U17): `--tenant <t>` lists that tenant's OPEN candidate queue with
309
+ // OPERATOR MODE: `--tenant <t>` lists that tenant's OPEN candidate queue with
296
310
  // NO checkout, over the same HTTP transport `tot ship --pr` uses. Only `list` has an
297
311
  // operator path today — `view`/`close` stay developer-only (checkout-bound).
298
312
  if (args.tenant && `${args.tenant}`.trim()) {
@@ -347,7 +361,7 @@ export async function run(argv, ctx) {
347
361
  const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
348
362
  const storefrontUrl = args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
349
363
  const statePath = defaultCandidateStatePath(env);
350
- // Branch-bound (u4): the active-pointer namespace is scoped to the current git
364
+ // Branch-bound: the active-pointer namespace is scoped to the current git
351
365
  // branch, so the "← active" marker reflects THIS branch's candidate.
352
366
  const scope = { mcpUrl: baseUrl, repo, branch: currentBranch(gitSafe) };
353
367
  const client = createMcpClient(baseUrl);
@@ -1,10 +1,10 @@
1
1
  /**
2
- * `tot preview build --tenant <t> --pr <N>` — OPERATOR build-on-demand (unit U3).
2
+ * `tot preview build --tenant <t> --pr <N>` — OPERATOR build-on-demand.
3
3
  *
4
4
  * Materialize ANY PR's hosted preview (including one that isn't yours, and one that
5
5
  * was orphaned — its webhook never processed so it has no ReviewEnvironment). It
6
- * calls the session-authenticated storefront endpoint `POST /api/preview/build`
7
- * (unit U1), which runs the EXISTING `reconcileCandidate` against the PR head. The
6
+ * calls the session-authenticated storefront endpoint `POST /api/preview/build`,
7
+ * which runs the EXISTING `reconcileCandidate` against the PR head. The
8
8
  * CLI passes the KNOWN descriptor (the head sha it holds), so there is NO forge
9
9
  * PR-read dependency: reconcile reads the tenant files at that sha (content-addressed).
10
10
  *
@@ -22,9 +22,9 @@
22
22
  * URL) and confirms before acting (`--yes` to skip; a non-TTY without `--yes`
23
23
  * aborts rather than acting silently). Build-on-demand is INERT — it flips no shared
24
24
  * channel and is not go-live — so this is a materialize, not a deploy. The plan
25
- * itself is built by the SHARED plan module (`../plan.mjs`, unit U10) — the same
26
- * affordance every other mutating operator verb (accept/ship/retire) and the
27
- * `/admin` confirm dialog use, per decision `operator-verb-and-hosting-model`.
25
+ * itself is built by the SHARED plan module (`../plan.mjs`) — the same
26
+ * affordance every other mutating operator verb (accept/ship/reject) and the
27
+ * `/admin` confirm dialog use.
28
28
  *
29
29
  * Dependency-free (global fetch + `git` for the optional head-sha default).
30
30
  */