@tokenoftrust/cli 1.4.0-rc.17 → 1.4.0-rc.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "1.4.0-rc.17",
3
+ "version": "1.4.0-rc.18",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -818,6 +818,7 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
818
818
  shipped: status?.shipped ?? null,
819
819
  dispatched: status?.dispatched ?? null,
820
820
  notDispatched: status?.notDispatched ?? false,
821
+ forwardFailed: status?.forwardFailed ?? false,
821
822
  delivery: status?.delivery ?? null,
822
823
  previewPrUrl,
823
824
  ...(error ? { error } : {}),
@@ -1008,7 +1009,15 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
1008
1009
  if (out.trim()) console.error(redactUrl(out.trim()));
1009
1010
  } catch (pushErr) {
1010
1011
  const msg = `push failed: ${redactUrl(String(pushErr.stderr || pushErr.message || pushErr))}`;
1011
- console.error(fail(msg, "check your commit and that the checkout's remote is reachable, then re-run"));
1012
+ // This is the NO-SESSION path pushing the clone-time embedded credential
1013
+ // which rotation kills the moment any fresh mint happens elsewhere. An auth
1014
+ // failure here is therefore almost always "you're not signed in IN THIS
1015
+ // SHELL", not a network problem; the old remote-is-reachable hint sent a
1016
+ // human down the wrong path live (Trello-13075 polish).
1017
+ const hint = isForgeAuthError(pushErr?.stderr || pushErr?.message || pushErr)
1018
+ ? "you're not signed in in this shell, and the checkout's embedded credential has likely been rotated — run `tot login` (check TOT_PROFILE if you use per-terminal identities), then re-run"
1019
+ : "check your commit and that the checkout's remote is reachable, then re-run";
1020
+ console.error(fail(msg, hint));
1012
1021
  emitJson(args, buildJsonResult({ ok: false, ref, commit, changeId, error: msg }));
1013
1022
  return 1;
1014
1023
  }
@@ -1016,7 +1025,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
1016
1025
  printChangeSummary(changeSummary, { quiet: args.json });
1017
1026
  const note = e instanceof AuthUnavailableError
1018
1027
  ? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
1019
- : `couldn't reach Token of Trust for the result read-back: ${String(e?.message || e)}`;
1028
+ : `couldn't reach Token of Trust for the result read-back: ${describeReadbackError(e)}`;
1020
1029
  if (!args.json) {
1021
1030
  console.log(` (${note})`);
1022
1031
  console.log(` Your push is in; the preview updates once reconcile completes.`);
@@ -1157,14 +1166,14 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
1157
1166
  }
1158
1167
  // --json also skips the browser auto-open (open: !args.noOpen && !args.json)
1159
1168
  // — automation doesn't want a browser popping up.
1160
- reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb });
1169
+ reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb, noChanges: patchEntries.length === 0 });
1161
1170
  emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
1162
1171
  return status?.status === "failed" ? 1 : 0;
1163
1172
  } catch (e) {
1164
1173
  progress?.stop();
1165
1174
  const note = e instanceof AuthUnavailableError
1166
1175
  ? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
1167
- : `reconcile is running — the result read-back isn't available yet: ${String(e?.message || e)}`;
1176
+ : `reconcile is running — the result read-back isn't available yet: ${describeReadbackError(e)}`;
1168
1177
  if (!args.json) {
1169
1178
  console.log(` (${note})`);
1170
1179
  console.log(` Your push is in; the preview updates once reconcile completes.`);
@@ -1250,6 +1259,19 @@ export async function pollPreviewStatus(
1250
1259
  if (last.dispatched) everDispatched = true;
1251
1260
  last.everDispatched = everDispatched;
1252
1261
  if (onTick) onTick(last, i);
1262
+ // Terminally-failed forward: the delivery record settled with forwarded:false
1263
+ // (and it isn't the at-receipt `pending` marker) — the control plane could not
1264
+ // deliver this commit to the reconciler, and polling longer cannot change that.
1265
+ // Only a NEW push produces a new delivery. Stop and say so (Trello-13075
1266
+ // honesty discipline: never spin on a state that cannot progress).
1267
+ if (
1268
+ last.status === "pending" &&
1269
+ last.delivery?.actual &&
1270
+ last.delivery.actual.forwarded === false &&
1271
+ !last.delivery.actual.pending
1272
+ ) {
1273
+ return { ...last, forwardFailed: true };
1274
+ }
1253
1275
  // Never-dispatched dead-end: still pending, no delivery has EVER been observed
1254
1276
  // for this commit, and we're past the startup grace — the reconcile will never
1255
1277
  // arrive. Return honestly instead of continuing to show "still reconciling".
@@ -1309,6 +1331,34 @@ export function shareablePrUrl(base, tenant, prNumber) {
1309
1331
  return `${String(base).replace(/\/+$/, "")}/preview/${tenant}/pr/${prNumber}`;
1310
1332
  }
1311
1333
 
1334
+ /**
1335
+ * Humanize a result-read-back failure. MCP auth errors arrive as a JSON blob
1336
+ * whose `data.self_repair` carries a summary + step list — dumping that raw
1337
+ * into the terminal (observed live: a wall of escaped JSON mid-submit) buries
1338
+ * the one thing the developer needs: sign in again. Detect that shape and
1339
+ * reduce it to the summary's first sentence + the concrete next step; anything
1340
+ * else passes through unchanged. Pure — unit-tested.
1341
+ * @param {unknown} e
1342
+ * @returns {string}
1343
+ */
1344
+ export function describeReadbackError(e) {
1345
+ const msg = String(e?.message || e || "");
1346
+ const jsonStart = msg.indexOf("{");
1347
+ if (jsonStart >= 0 && msg.includes("self_repair")) {
1348
+ try {
1349
+ const body = JSON.parse(msg.slice(jsonStart));
1350
+ const repair = body?.data?.self_repair;
1351
+ const summaryFirst = String(repair?.summary || body?.message || "").split(/(?<=\.)\s/)[0];
1352
+ if (summaryFirst) {
1353
+ return `${summaryFirst} Next: run \`tot login\` in this shell (check TOT_PROFILE), then re-run.`;
1354
+ }
1355
+ } catch {
1356
+ // Not the shape we thought — fall through to the raw message.
1357
+ }
1358
+ }
1359
+ return msg;
1360
+ }
1361
+
1312
1362
  /**
1313
1363
  * Build the printed lines for the headline "share this with your reviewer" block —
1314
1364
  * the whole point of U14: on a successful preview, the SHAREABLE deep link
@@ -1352,14 +1402,24 @@ export function formatShareableUrlBlock(s, tenant) {
1352
1402
  * @param {string} tenant @param {string} [verb]
1353
1403
  * @returns {string[]}
1354
1404
  */
1355
- export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, tenant, verb = "preview") {
1405
+ export function formatNotDispatchedBlock({ commit = null, ref = null, noChanges = false } = {}, tenant, verb = "preview") {
1356
1406
  const short = commit ? commit.slice(0, 9) : "(unknown commit)";
1407
+ // Empty-diff cause FIRST when we know it applies (live-testing finding: an
1408
+ // empty candidate submit structurally CANNOT build — no diff → no candidate PR
1409
+ // → no pull_request webhook — and blaming webhooks/scope for it sent a human
1410
+ // down two wrong debugging paths).
1411
+ const causes = [
1412
+ ...(noChanges
1413
+ ? [` • your submit contained NO content changes — a candidate with no diff opens no PR and builds nothing (make an edit, or move the shared ref: \`tot ${verb} --ref preview\`),`]
1414
+ : []),
1415
+ ` • the store's reconcile webhook isn't registered yet (an operator must (re-)provision it), or`,
1416
+ ` • your session is scoped to a different store than the one you pushed.`,
1417
+ ];
1357
1418
  return [
1358
1419
  `\n ⚠ No reconcile was dispatched for ${short} on ${tenant}.`,
1359
1420
  ` Your push landed${ref ? ` on ${ref}` : ""}, but nothing picked it up to build a preview —`,
1360
1421
  ` re-running \`tot ${verb}\` will NOT change that. This usually means one of:`,
1361
- ` • the store's reconcile webhook isn't registered yet (an operator must (re-)provision it), or`,
1362
- ` • your session is scoped to a different store than the one you pushed.`,
1422
+ ...causes,
1363
1423
  ` Next:`,
1364
1424
  ` • \`tot grants\` — confirm ${tenant} is active for you;`,
1365
1425
  ` • check the preview dashboard for ${tenant} (it will read "Last reconcile: never" until a job runs);`,
@@ -1367,6 +1427,26 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
1367
1427
  ];
1368
1428
  }
1369
1429
 
1430
+ /**
1431
+ * The honest "the delivery FAILED to forward" block — the delivery record settled
1432
+ * `forwarded: false` (not the at-receipt pending marker), so the control plane
1433
+ * could not deliver this commit to the reconciler; polling longer cannot change
1434
+ * that, and ONLY a new push produces a new delivery. Distinct from
1435
+ * `formatNotDispatchedBlock` (nothing was ever dispatched) — here the plumbing
1436
+ * fired and died in transit, so the recovery differs. Pure — unit-tested.
1437
+ * @param {{ commit?: string|null }} ctx @param {string} tenant
1438
+ * @returns {string[]}
1439
+ */
1440
+ export function formatForwardFailedBlock({ commit = null } = {}, tenant) {
1441
+ const short = commit ? commit.slice(0, 9) : "(unknown commit)";
1442
+ return [
1443
+ `\n ⚠ The reconcile delivery for ${short} on ${tenant} FAILED in transit (network/timeout at the control plane).`,
1444
+ ` Waiting longer will not help — only a NEW push produces a new delivery.`,
1445
+ ` Next: commit again (an empty commit works: git commit --allow-empty -m retry) and re-push;`,
1446
+ ` if it fails the same way twice, share this with support: commit ${short}, tenant ${tenant}.`,
1447
+ ];
1448
+ }
1449
+
1370
1450
  /**
1371
1451
  * Print the reconcile/compliance/preview result and, on a clean reconcile with a
1372
1452
  * preview URL, open it in the browser (unless opts.open === false). `quiet`
@@ -1375,7 +1455,7 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
1375
1455
  * --json — automation doesn't want a browser popping up). `commit`/`ref`/`verb`
1376
1456
  * feed the honest never-dispatched block.
1377
1457
  */
1378
- function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview" } = {}) {
1458
+ function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview", noChanges = false } = {}) {
1379
1459
  if (!s || s.status === "unknown") {
1380
1460
  if (!quiet) {
1381
1461
  console.log(
@@ -1385,9 +1465,16 @@ function reportStatus(s, tenant, { open = true, quiet = false, commit = null, re
1385
1465
  }
1386
1466
  return;
1387
1467
  }
1468
+ // Terminally-failed forward — the delivery fired and died in transit; a re-push
1469
+ // (new delivery) is the only recovery. Checked BEFORE notDispatched: a settled
1470
+ // failed forward IS a dispatch, just a doomed one.
1471
+ if (s.forwardFailed) {
1472
+ if (!quiet) for (const line of formatForwardFailedBlock({ commit }, tenant)) console.log(line);
1473
+ return;
1474
+ }
1388
1475
  // Never-dispatched dead-end — the honest replacement for false "still reconciling".
1389
1476
  if (s.notDispatched) {
1390
- if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref }, tenant, verb)) console.log(line);
1477
+ if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref, noChanges }, tenant, verb)) console.log(line);
1391
1478
  return;
1392
1479
  }
1393
1480
  if (s.status === "pending") {