@cotal-ai/cli 0.153.1 → 0.155.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 (78) hide show
  1. package/dist/command.d.ts.map +1 -1
  2. package/dist/command.js +7 -6
  3. package/dist/command.js.map +1 -1
  4. package/dist/commands/mint.js +1 -1
  5. package/dist/commands/mint.js.map +1 -1
  6. package/dist/commands/reset-space-apply.d.ts.map +1 -1
  7. package/dist/commands/reset-space-apply.js +5 -2
  8. package/dist/commands/reset-space-apply.js.map +1 -1
  9. package/dist/commands/space-registration.d.ts.map +1 -1
  10. package/dist/commands/space-registration.js +2 -3
  11. package/dist/commands/space-registration.js.map +1 -1
  12. package/dist/commands/space.d.ts +5 -3
  13. package/dist/commands/space.d.ts.map +1 -1
  14. package/dist/commands/space.js +16 -9
  15. package/dist/commands/space.js.map +1 -1
  16. package/dist/commands/spawn-preview.d.ts.map +1 -1
  17. package/dist/commands/spawn-preview.js +4 -8
  18. package/dist/commands/spawn-preview.js.map +1 -1
  19. package/dist/commands/spawn.d.ts.map +1 -1
  20. package/dist/commands/spawn.js +11 -1
  21. package/dist/commands/spawn.js.map +1 -1
  22. package/dist/commands/stack-down.d.ts.map +1 -1
  23. package/dist/commands/stack-down.js +13 -8
  24. package/dist/commands/stack-down.js.map +1 -1
  25. package/dist/commands/stack-lost-records.d.ts +7 -0
  26. package/dist/commands/stack-lost-records.d.ts.map +1 -0
  27. package/dist/commands/stack-lost-records.js +143 -0
  28. package/dist/commands/stack-lost-records.js.map +1 -0
  29. package/dist/commands/stack-up.d.ts +3 -3
  30. package/dist/commands/stack-up.d.ts.map +1 -1
  31. package/dist/commands/stack-up.js +20 -12
  32. package/dist/commands/stack-up.js.map +1 -1
  33. package/dist/commands/status.d.ts +2 -2
  34. package/dist/commands/status.d.ts.map +1 -1
  35. package/dist/commands/status.js +90 -44
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +8 -6
  39. package/dist/commands/sync.js.map +1 -1
  40. package/dist/commands/team-boot-controller.d.ts.map +1 -1
  41. package/dist/commands/team-boot-controller.js +3 -1
  42. package/dist/commands/team-boot-controller.js.map +1 -1
  43. package/dist/commands/team.d.ts.map +1 -1
  44. package/dist/commands/team.js +6 -2
  45. package/dist/commands/team.js.map +1 -1
  46. package/dist/commands/update.d.ts +1 -0
  47. package/dist/commands/update.d.ts.map +1 -1
  48. package/dist/commands/update.js +3 -3
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/whoami.d.ts.map +1 -1
  51. package/dist/commands/whoami.js +7 -11
  52. package/dist/commands/whoami.js.map +1 -1
  53. package/dist/help.d.ts.map +1 -1
  54. package/dist/help.js +11 -2
  55. package/dist/help.js.map +1 -1
  56. package/dist/lib/controller-resume.d.ts.map +1 -1
  57. package/dist/lib/controller-resume.js +2 -1
  58. package/dist/lib/controller-resume.js.map +1 -1
  59. package/dist/lib/manifest/prepare.d.ts.map +1 -1
  60. package/dist/lib/manifest/prepare.js +4 -0
  61. package/dist/lib/manifest/prepare.js.map +1 -1
  62. package/dist/lib/persona-files.js +4 -4
  63. package/dist/lib/persona-files.js.map +1 -1
  64. package/dist/lib/restore.d.ts.map +1 -1
  65. package/dist/lib/restore.js +3 -1
  66. package/dist/lib/restore.js.map +1 -1
  67. package/dist/lib/service-presets.d.ts.map +1 -1
  68. package/dist/lib/service-presets.js +7 -1
  69. package/dist/lib/service-presets.js.map +1 -1
  70. package/dist/lib/stack-journal.d.ts +16 -0
  71. package/dist/lib/stack-journal.d.ts.map +1 -1
  72. package/dist/lib/stack-journal.js +39 -8
  73. package/dist/lib/stack-journal.js.map +1 -1
  74. package/dist/lib/store-identity.d.ts +15 -0
  75. package/dist/lib/store-identity.d.ts.map +1 -0
  76. package/dist/lib/store-identity.js +30 -0
  77. package/dist/lib/store-identity.js.map +1 -0
  78. package/package.json +16 -4
@@ -2,7 +2,7 @@ import { accessSync, constants, existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { CotalEndpoint, agentFilePath, dialerFor, EpEnvelopeError, formatAge, isReachable, mintCreds, mintLifecycleUid, resolveService, realDirNoSymlink, invokeRepairingSplit, standaloneConnectOpts, newIdentity, idFromCreds, principalKey, DEV_OWNER, unansweredRequest, resolveAuthProvider, personaDir, presenceText, } from "@cotal-ai/core";
5
- import { stoppedLoopbackListener, accountInventory, assessMaintenanceClaim, authDir, isCommandFailure, readMaintenanceJournal, recordsElsewhereError, recordsElsewhereText, restoreRolledBackRuns, targetErrorSpace, brokerAddressForDisplay, canonicalRoot, CLI_USER_ACTOR, codeSpan, CommandFailure, deliveryCredsKey, DELIVERY_PIDFILE, extensionsDir, findCotalRoot, folderSpaceOverSelection, meshesForRoot, resolveReadsLiveness, stoppedStack, unsettledStackOperation, withdrawOperationNote, hasUserAuthState, homeCotalDir, cachedPreferences, loadLaunchPersonaFile, resolvePreference, isMeshRegistryError, isWorkspaceTargetError, jsonFlag, loadManagerInstanceIdentity, loadSoleSpaceAuth, localProcessPath, localProcessVisible, MANAGER_PIDFILE, oneLine, PREFERENCE_KEYS, parsePid, plainFlag, preflightTarget, presentationOf, principalFromBearer, probeLiveness, probeWebReadiness, readCurrent, readMeshRegistry, readPidfile, preparedRecordSkipped, readPreparedSpaces, readRenewalRecord, renderWorkspaceError, resolveMeshTarget, serverFlag, spaceFlag, userAuthStateDir, WEB_PIDFILE, WEB_READINESS_HEADER, workspaceSecretStore, writeResult, workspaceErrorParts, preferencesPath, preferencesFolderWarning, cotalDirLink, movedProject, spaceStreamsAbsent, spaceStreamsExpected, resolveControlTarget, } from "@cotal-ai/workspace";
5
+ import { stoppedLoopbackListener, accountInventory, assessMaintenanceClaim, authDir, isCommandFailure, readMaintenanceJournal, recordsElsewhereError, recordsElsewhereText, restoreRolledBackRuns, targetErrorSpace, brokerAddressForDisplay, brokerCheck, canonicalRoot, CLI_USER_ACTOR, codeSpan, CommandFailure, deliveryCredsKey, DELIVERY_PIDFILE, extensionsDir, findCotalRoot, folderSpaceOverSelection, meshesForRoot, resolveReadsLiveness, stoppedStack, unsettledStackOperation, withdrawOperationNote, hasUserAuthState, homeCotalDir, cachedPreferences, loadLaunchPersonaFile, resolvePreference, isMeshRegistryError, isWorkspaceTargetError, jsonFlag, loadManagerInstanceIdentity, loadSoleSpaceAuth, localProcessPath, localProcessVisible, MANAGER_PIDFILE, oneLine, PREFERENCE_KEYS, parsePid, plainFlag, preflightTarget, presentationOf, principalFromBearer, probeLiveness, probeWebReadiness, readCurrent, readMeshRegistry, readPidfile, preparedRecordSkipped, readPreparedSpaces, readRenewalRecord, renderWorkspaceError, resolveMeshTarget, serverFlag, spaceFlag, userAuthStateDir, WEB_PIDFILE, WEB_READINESS_HEADER, workspaceSecretStore, writeResult, workspaceErrorParts, preferencesPath, preferencesFolderWarning, cotalDirLink, movedProject, spaceStreamsAbsent, spaceStreamsExpected, resolveControlTarget, } from "@cotal-ai/workspace";
6
6
  import { localProcessSurface } from "../ext-loader.js";
7
7
  import { cliVersion, cliProvenance, readExtensionVersions } from "../lib/version.js";
8
8
  import { agentSkillsSkew } from "../lib/agent-skills.js";
@@ -30,9 +30,10 @@ import { freshTeamAttemptSettled, listFreshTeamAttempts, listFreshTeamSummaries
30
30
  import { additiveTeamRecordedState, listAdditiveTeamAttempts } from "../lib/team-additive-attempt.js";
31
31
  import { maintenanceNextStep, preservedSourceState, restoreNextStep, rolledBackNotes } from "../lib/restore.js";
32
32
  import { attemptProcessesGone } from "../lib/isolated-broker.js";
33
- import { journalRecordedElsewhere, readStackJournal, serviceHeldRecords, STACK_DEFINITION, stackStartedWithoutManager, stackOperationBlock, goneStackRoot, unfinishedStackOperation, } from "../lib/stack-journal.js";
33
+ import { journalRecordedElsewhere, readStackJournal, serviceHeldRecords, STACK_DEFINITION, stackStartedWithoutManager, stackOperationBlock, stackRootGone, unfinishedStackOperation, } from "../lib/stack-journal.js";
34
34
  import { cotalLinkHere, homeHoldsStack, linkedCotalRemedy } from "../lib/paths.js";
35
35
  import { storeWriteProblem } from "../lib/store-dir.js";
36
+ import { stackStoreWarnings } from "../lib/store-identity.js";
36
37
  import { natsServerLabel } from "../lib/nats-bin.js";
37
38
  import { askManager } from "@cotal-ai/workspace/control";
38
39
  /** `--components` is the fail-loud health pass with a machine-readable EXIT DISPOSITION. Bare
@@ -59,21 +60,20 @@ export const statusFlags = [
59
60
  jsonFlag,
60
61
  plainFlag,
61
62
  ];
62
- /** A project root with no registration or prepared space here, such as one whose space was
63
- * forgotten. The resolver gives it the default broker address, which any broker may hold, so
64
- * status selects no space for it and dials nothing. */
63
+ /** A project root with an empty `.cotal/` and no registration or prepared space. The resolver
64
+ * gives it the default broker address, which any broker may hold, so status selects no space for
65
+ * it and dials nothing. A root whose records name no space is refused by the resolver instead, and
66
+ * status uses that refusal. */
65
67
  class UnregisteredRoot {
66
68
  root;
67
- namesNoSpace;
68
- constructor(root, namesNoSpace) {
69
+ constructor(root) {
69
70
  this.root = root;
70
- this.namesNoSpace = namesNoSpace;
71
71
  }
72
72
  }
73
73
  function resolveSelected(cwd, values, offline) {
74
74
  try {
75
75
  const target = resolveMeshTarget(cwd, { server: values.server, space: values.space, offline });
76
- return target.source === "local-space" ? { ok: false, error: new UnregisteredRoot(target.root, target.namesNoSpace === true) } : { ok: true, target };
76
+ return target.source === "local-space" ? { ok: false, error: new UnregisteredRoot(target.root) } : { ok: true, target };
77
77
  }
78
78
  catch (e) {
79
79
  return { ok: false, error: e };
@@ -162,10 +162,24 @@ export async function status(args) {
162
162
  // THE RESPONDER AXIS (#1576), read ONCE and threaded into the rows that would otherwise claim
163
163
  // health they never checked. It is read before the folder section because that section renders
164
164
  // the delivery process row, and a live pid means nothing without it.
165
- const responder = await readResponderAxis(selected);
165
+ const preflight = selected.ok && selected.target.mode !== "user" ? await preflightTarget(selected.target) : undefined;
166
+ const responder = await readResponderAxis(selected, preflight);
166
167
  const folder = blocked ? unreadFolder(root, blocked) : await readFolder(root, selected, values, responder.state, gaps, hints);
167
- const meshes = await readMeshes(cwd, selected, Boolean(values.all), gaps, offline);
168
- const target = await readSelected(selected, responder, folder, gaps, hints);
168
+ const meshes = await readMeshes(cwd, selected, Boolean(values.all), gaps, offline, preflight);
169
+ const maintenance = blocked ? { root, space: null, state: "unreadable", attemptId: null, next: null } : readMaintenance(root, gaps);
170
+ const recovery = backupRecovery(maintenance);
171
+ const selectedHints = [];
172
+ const target = await readSelected(selected, responder, folder, gaps, selectedHints, preflight);
173
+ // A held backup must be released before this root can start. Keep the selection's diagnostics,
174
+ // but replace its ordinary restart advice with the journal's recovery step.
175
+ if (recovery)
176
+ hints.unshift(recovery);
177
+ else
178
+ hints.push(...selectedHints);
179
+ if (!blocked)
180
+ readStackStoreWarnings(root, gaps, hints);
181
+ if (selected.ok && selected.target.origin === "up" && !sameDir(root, selected.target.root))
182
+ readStackStoreWarnings(selected.target.root, gaps, hints, selected.target.space);
169
183
  // The note under the heading sends here for this folder's own stack cut off with no recorded
170
184
  // outcome under another selection. A blocked one is named with its block under `This folder's
171
185
  // space` and its step first in Next, so that note is left out here, with its --json warning. One
@@ -173,11 +187,10 @@ export async function status(args) {
173
187
  const held = folderStack(folder, target);
174
188
  if (held && folder.stackBlock)
175
189
  withdrawOperationNote("FOLDER_SPACE_NOT_RUNNING", held.space);
176
- if (held && !folder.stackBlock && !folder.stackUnfinished && unsettledStackOperation(held.root, held.space))
190
+ if (!recovery && held && !folder.stackBlock && !folder.stackUnfinished && unsettledStackOperation(held.root, held.space))
177
191
  hints.push(`start it with \`cotal up\` in ${held.root}`);
178
192
  const services = readOwnedServices(selected, gaps);
179
193
  const bootAttempts = blocked ? { root, state: "partial", observedAt: new Date().toISOString(), unsettled: [], additive: [], unreadable: [], elsewhere: [] } : readBootAttempts(root, gaps);
180
- const maintenance = blocked ? { root, space: null, state: "unreadable", attemptId: null, next: null } : readMaintenance(root, gaps);
181
194
  const logged = selected.ok ? brokerStoreWrite(selected.target.root, selected.target.space) : undefined;
182
195
  // A store write failure holds until the broker restarts, so it is named before a full store.
183
196
  const full = selected.ok && target.resolved && target.storedBytes !== null && logged?.state !== "write-failed" ? storeFull(selected.target.root, selected.target.space, target.storedBytes) : undefined;
@@ -476,7 +489,7 @@ async function expectedDeliveryHolder(target) {
476
489
  *
477
490
  * `leased` says whether the read found a delivery lease record. An open mesh runs no delivery
478
491
  * daemon, so there a missing lease is the normal state rather than an unbound responder. */
479
- async function readResponderAxis(selected) {
492
+ async function readResponderAxis(selected, preflight) {
480
493
  let leased = false;
481
494
  if (!selected.ok)
482
495
  return { state: "unknown", leased };
@@ -484,8 +497,7 @@ async function readResponderAxis(selected) {
484
497
  if (target.mode === "user")
485
498
  return { state: "unknown", leased };
486
499
  try {
487
- const preflight = await preflightTarget(target);
488
- if (!preflight.ok)
500
+ if (!preflight?.ok)
489
501
  return { state: "unknown", leased };
490
502
  const id = newIdentity();
491
503
  const creds = target.auth ? await mintCreds(target.auth, id, "observer") : undefined;
@@ -646,9 +658,9 @@ async function readFolder(root, selected, values, responder, gaps, hints) {
646
658
  controllerStack = false;
647
659
  // A journal no command can use is named with its way out, in the warnings and under Next; the
648
660
  // rest of the report still stands.
649
- if (isCommandFailure(error) && error.code === "SCHEMA_INVALID") {
661
+ if (isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED")) {
650
662
  unusable = true;
651
- gaps.add("STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
663
+ gaps.add(error.code === "PERMISSION_DENIED" ? "STACK_JOURNAL_UNREADABLE" : "STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
652
664
  if (error.remedy)
653
665
  hints.push(error.remedy);
654
666
  }
@@ -929,7 +941,7 @@ function deliveryDetail(live, responder) {
929
941
  * with its own recorded TLS requirement. A bare TCP/INFO probe would green-light a plaintext broker
930
942
  * that has substituted for a TLS-required mesh — the FAIL1 attack — so a list that only asks "is
931
943
  * anything listening" cannot report on the one property the record claims. */
932
- async function readMeshes(cwd, selected, all, gaps, offline) {
944
+ async function readMeshes(cwd, selected, all, gaps, offline, preflight) {
933
945
  // A registry file that cannot be read is named, and the readable records are still listed and
934
946
  // checked. Only an absent directory or pointer is an empty registry or no current mesh.
935
947
  const unreadable = [];
@@ -971,6 +983,18 @@ async function readMeshes(cwd, selected, all, gaps, offline) {
971
983
  const chosen = selected.ok ? selected.target : undefined;
972
984
  // A `--server` that names another address is the only broker dialed for the selected space.
973
985
  const checked = (m) => all || (m.space === chosen?.space && m.server === chosen.server);
986
+ const check = async (m) => {
987
+ if (!checked(m))
988
+ return "not-checked";
989
+ if (preflight && m.space === chosen?.space && m.server === chosen.server) {
990
+ if (preflight.ok)
991
+ return "reachable";
992
+ if (preflight.kind === "slow-link" || preflight.kind === "broker-stopped" || preflight.kind === "unreachable")
993
+ return preflight.kind;
994
+ return preflight.kind === "tls-trust" || preflight.kind === "tls-unsupported" ? "unreachable" : "reachable";
995
+ }
996
+ return brokerCheck(m.server, m.tlsRequired);
997
+ };
974
998
  const items = await Promise.all(entries.map(async (m) => ({
975
999
  space: m.space,
976
1000
  server: displayStatusBroker(m.server, m.space, gaps),
@@ -981,7 +1005,7 @@ async function readMeshes(cwd, selected, all, gaps, offline) {
981
1005
  eventsRequired: m.policy?.events === "required",
982
1006
  modeChecked: m.unchecked !== true,
983
1007
  current: m.space === current,
984
- check: checked(m) ? ((await isReachable(m.server, m.tlsRequired ? { tls: true } : {})) ? "reachable" : "unreachable") : "not-checked",
1008
+ check: await check(m),
985
1009
  catalog: m.origin === "catalog" ? { name: m.catalogName && m.catalogName !== m.space ? m.catalogName : null, fetchedAt: m.catalogFetchedAt ?? null, error: m.catalogError ?? null } : null,
986
1010
  })));
987
1011
  // The prepared spaces folder, or a record in it, that cannot be read is named the same way.
@@ -1014,6 +1038,25 @@ async function readMeshes(cwd, selected, all, gaps, offline) {
1014
1038
  catalogs: preparedCatalogDiagnostics().map((r) => ({ idp: r.account.idpUrl, account: r.account.sub, state: r.state, error: r.state === "failed" ? (r.error ?? "refresh failed") : null })),
1015
1039
  };
1016
1040
  }
1041
+ /** Diagnose the current folder's store and a selected local stack independently of folder trust. */
1042
+ function readStackStoreWarnings(root, gaps, hints, space) {
1043
+ try {
1044
+ const journal = readStackJournal(root)?.journal;
1045
+ if (journal && (space === undefined || journal.space === space))
1046
+ for (const warning of stackStoreWarnings(journal))
1047
+ gaps.add(warning.code, warning.message, warning.target);
1048
+ }
1049
+ catch (error) {
1050
+ if (isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED")) {
1051
+ gaps.add(error.code === "PERMISSION_DENIED" ? "STACK_JOURNAL_UNREADABLE" : "STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
1052
+ if (error.remedy)
1053
+ hints.push(error.remedy);
1054
+ }
1055
+ else if (!isCommandFailure(error) || error.code !== "CONFLICT")
1056
+ throw error;
1057
+ // A copied or moved journal is diagnosed by the folder and selection readers.
1058
+ }
1059
+ }
1017
1060
  /** The refusal of the stack journal at `root` when it is one no command can use, as `up` and `down`
1018
1061
  * refuse it. Undefined when it reads or is absent. */
1019
1062
  function unusableStackJournal(root) {
@@ -1022,7 +1065,7 @@ function unusableStackJournal(root) {
1022
1065
  return undefined;
1023
1066
  }
1024
1067
  catch (error) {
1025
- return isCommandFailure(error) && error.code === "SCHEMA_INVALID" ? error : undefined;
1068
+ return isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED") ? error : undefined;
1026
1069
  }
1027
1070
  }
1028
1071
  /** The refusal of `space`'s service instance records when one is a record no command can use, as
@@ -1053,7 +1096,7 @@ function rootStoreProblem(root) {
1053
1096
  return undefined;
1054
1097
  }
1055
1098
  }
1056
- async function readSelected(selected, responder, folder, gaps, hints) {
1099
+ async function readSelected(selected, responder, folder, gaps, hints, preflight) {
1057
1100
  const cmd = displayCmd();
1058
1101
  if (!selected.ok) {
1059
1102
  const e = selected.error;
@@ -1074,11 +1117,7 @@ async function readSelected(selected, responder, folder, gaps, hints) {
1074
1117
  }
1075
1118
  if (e instanceof UnregisteredRoot) {
1076
1119
  const message = `no space is registered or prepared here for ${e.root}`;
1077
- // A bare `up` in a folder whose records name no space starts a space named after the folder,
1078
- // which then holds it, so the step that brings a forgotten space back is to prepare it again.
1079
- hints.push(e.namesNoSpace
1080
- ? `prepare a space with \`${cmd} space create <name> --root ${shellWord(e.root)}\`, or select one with \`${cmd} use <name>\``
1081
- : `run \`${cmd} up\` in ${e.root} to start one, or select a space with \`${cmd} use <name>\``);
1120
+ hints.push(`run \`${cmd} up\` in ${e.root} to start one, or select a space with \`${cmd} use <name>\``);
1082
1121
  return { resolved: false, code: "unregistered-root", message };
1083
1122
  }
1084
1123
  if (!isWorkspaceTargetError(e))
@@ -1145,6 +1184,16 @@ async function readSelected(selected, responder, folder, gaps, hints) {
1145
1184
  spaceStreams: { state: "not-checked", root: null, stack: null },
1146
1185
  storedBytes: null,
1147
1186
  };
1187
+ // A moved project keeps its trust under its new path. Prefer that specific diagnosis when
1188
+ // this folder holds the records, otherwise report the missing stack root before any auth probe.
1189
+ const moved = target.mode === "auth" && !target.auth ? movedProject(target.space, target.root) : undefined;
1190
+ if (moved) {
1191
+ hints.push(moved.remedy);
1192
+ return { resolved: false, code: "moved-folder", message: moved.message };
1193
+ }
1194
+ const overridden = (target.source === "flag-server" || target.source === "flag-space-override") && target.recordedServer !== target.server;
1195
+ if (base.rootGone && !overridden)
1196
+ return { ...base, connection: "root-gone", snapshot: { state: "skipped", reason: "the stack's project folder was moved or deleted" } };
1148
1197
  if (target.mode === "user") {
1149
1198
  // Status never static-mints on a user-auth mesh (the flip). Everything here is offline
1150
1199
  // introspection — the login cache + the locally-readable ledger — plus, only when this
@@ -1177,14 +1226,8 @@ async function readSelected(selected, responder, folder, gaps, hints) {
1177
1226
  return { ...result, snapshot: { state: "skipped", reason: "needs a signed-in, granted login" } };
1178
1227
  return { ...result, ...(await readSnapshot(() => userSnapshotEndpoint(target, stateDir), target.space, true, gaps)) };
1179
1228
  }
1180
- // A project folder moved after its stack started there keeps the seed under its new path, where a
1181
- // probe without it reads as authentication required. Name the move and the way back, as send does.
1182
- const moved = target.mode === "auth" && !target.auth ? movedProject(target.space, target.root) : undefined;
1183
- if (moved) {
1184
- hints.push(moved.remedy);
1185
- return { resolved: false, code: "moved-folder", message: moved.message };
1186
- }
1187
- const preflight = await preflightTarget(target);
1229
+ if (preflight === undefined)
1230
+ throw new Error("status: selected non-user target has no preflight observation");
1188
1231
  if (!preflight.ok) {
1189
1232
  // A `--server` address that is not the one recorded for the space says nothing about whether its
1190
1233
  // stack runs, so Next names that address as the refusal of other commands does. A stopped stack
@@ -1409,7 +1452,7 @@ function folderSection(f, selected) {
1409
1452
  return { title: "This Folder", blocks: [{ kind: "fields", rows }] };
1410
1453
  }
1411
1454
  function meshesSections(m) {
1412
- const check = { reachable: cell("reachable", "ok"), unreachable: cell("down", "failed"), "not-checked": cell("not checked", "hint") };
1455
+ const check = { reachable: cell("reachable", "ok"), unreachable: cell("down", "failed"), "slow-link": cell("not answering", "failed"), "broker-stopped": cell("broker process is stopped", "failed"), "not-checked": cell("not checked", "hint") };
1413
1456
  const blocks = [];
1414
1457
  if (!m.items.length && !m.unreadable.length)
1415
1458
  blocks.push({ kind: "lines", lines: [cell("none", "hint")] });
@@ -1608,17 +1651,13 @@ function machineOverview(r, wide) {
1608
1651
  ],
1609
1652
  };
1610
1653
  }
1611
- /** Whether `root` is the gone folder of the stack this machine still records for `space`. */
1612
- function stackRootGone(space, root) {
1613
- const gone = goneStackRoot(space);
1614
- return gone !== undefined && canonicalRoot(gone) === canonicalRoot(root);
1615
- }
1616
1654
  function connectionCell(s) {
1617
1655
  if (s.connection === "ok" || s.connection === "reachable")
1618
1656
  return stateCell("reachable", "ok");
1619
1657
  const states = {
1658
+ "root-gone": "project folder was moved or deleted",
1620
1659
  unreachable: "unreachable",
1621
- "slow-link": "connection timed out",
1660
+ "slow-link": "not answering",
1622
1661
  "broker-stopped": `broker process ${s.brokerPid} is stopped`,
1623
1662
  "tls-trust": "TLS could not be verified",
1624
1663
  "tls-unsupported": "listener does not offer required TLS",
@@ -1655,7 +1694,7 @@ function spaceStateCell(r, entry) {
1655
1694
  return connectionCell(r.selected);
1656
1695
  if (entry.check === "not-checked")
1657
1696
  return "";
1658
- return stateCell(entry.check === "reachable" ? "reachable" : "unreachable", entry.check === "reachable" ? "ok" : "failed");
1697
+ return stateCell(entry.check === "slow-link" ? "not answering" : entry.check === "broker-stopped" ? "broker process is stopped" : entry.check, entry.check === "reachable" ? "ok" : "failed");
1659
1698
  }
1660
1699
  /**
1661
1700
  * The spaces this machine knows: the selected one first with its connection state, then one line
@@ -1818,7 +1857,14 @@ function stackLines(folder) {
1818
1857
  function roleList(roles) {
1819
1858
  return roles.length > 1 ? `${roles.slice(0, -1).join(", ")} and ${roles.at(-1)}` : roles[0] ?? "";
1820
1859
  }
1860
+ /** Pre-commit backup claims block ordinary startup; ready cuts and committed restores do not. */
1861
+ function backupRecovery(maintenance) {
1862
+ return maintenance?.state === "claimed" || maintenance?.state === "restore-ready" ? maintenance.next : null;
1863
+ }
1821
1864
  function nextActions(r) {
1865
+ const recovery = backupRecovery(r.maintenance);
1866
+ if (recovery)
1867
+ return { title: "Next", blocks: [...new Set([recovery, ...r.hints])].map((hint) => ({ kind: "lines", lines: [cell(hint, "hint")] })) };
1822
1868
  const cmd = displayCmd();
1823
1869
  const items = [];
1824
1870
  const extra = [];