@cotal-ai/cli 0.154.0 → 0.155.1

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 (76) hide show
  1. package/dist/commands/agents.js +3 -3
  2. package/dist/commands/agents.js.map +1 -1
  3. package/dist/commands/space-registration.d.ts.map +1 -1
  4. package/dist/commands/space-registration.js +17 -6
  5. package/dist/commands/space-registration.js.map +1 -1
  6. package/dist/commands/space.d.ts +2 -0
  7. package/dist/commands/space.d.ts.map +1 -1
  8. package/dist/commands/space.js +5 -1
  9. package/dist/commands/space.js.map +1 -1
  10. package/dist/commands/spawn-preview.d.ts.map +1 -1
  11. package/dist/commands/spawn-preview.js +4 -8
  12. package/dist/commands/spawn-preview.js.map +1 -1
  13. package/dist/commands/stack-down.d.ts.map +1 -1
  14. package/dist/commands/stack-down.js +13 -8
  15. package/dist/commands/stack-down.js.map +1 -1
  16. package/dist/commands/stack-lost-records.d.ts +7 -0
  17. package/dist/commands/stack-lost-records.d.ts.map +1 -0
  18. package/dist/commands/stack-lost-records.js +143 -0
  19. package/dist/commands/stack-lost-records.js.map +1 -0
  20. package/dist/commands/stack-up.d.ts +3 -3
  21. package/dist/commands/stack-up.d.ts.map +1 -1
  22. package/dist/commands/stack-up.js +20 -12
  23. package/dist/commands/stack-up.js.map +1 -1
  24. package/dist/commands/status.d.ts +4 -0
  25. package/dist/commands/status.d.ts.map +1 -1
  26. package/dist/commands/status.js +89 -37
  27. package/dist/commands/status.js.map +1 -1
  28. package/dist/commands/sync.d.ts.map +1 -1
  29. package/dist/commands/sync.js +8 -6
  30. package/dist/commands/sync.js.map +1 -1
  31. package/dist/commands/team-boot-controller.d.ts.map +1 -1
  32. package/dist/commands/team-boot-controller.js +3 -1
  33. package/dist/commands/team-boot-controller.js.map +1 -1
  34. package/dist/commands/team-channel-remove.d.ts +2 -2
  35. package/dist/commands/team.d.ts.map +1 -1
  36. package/dist/commands/team.js +6 -2
  37. package/dist/commands/team.js.map +1 -1
  38. package/dist/commands/whoami.d.ts.map +1 -1
  39. package/dist/commands/whoami.js +7 -11
  40. package/dist/commands/whoami.js.map +1 -1
  41. package/dist/help.d.ts.map +1 -1
  42. package/dist/help.js +11 -2
  43. package/dist/help.js.map +1 -1
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/controller-resume.d.ts.map +1 -1
  49. package/dist/lib/controller-resume.js +2 -1
  50. package/dist/lib/controller-resume.js.map +1 -1
  51. package/dist/lib/persona-files.d.ts.map +1 -1
  52. package/dist/lib/persona-files.js +26 -11
  53. package/dist/lib/persona-files.js.map +1 -1
  54. package/dist/lib/restore.d.ts.map +1 -1
  55. package/dist/lib/restore.js +3 -1
  56. package/dist/lib/restore.js.map +1 -1
  57. package/dist/lib/self-exec.d.ts +2 -2
  58. package/dist/lib/self-exec.d.ts.map +1 -1
  59. package/dist/lib/self-exec.js +6 -3
  60. package/dist/lib/self-exec.js.map +1 -1
  61. package/dist/lib/service-presets.d.ts.map +1 -1
  62. package/dist/lib/service-presets.js +7 -1
  63. package/dist/lib/service-presets.js.map +1 -1
  64. package/dist/lib/service-systemd-files.d.ts +3 -0
  65. package/dist/lib/service-systemd-files.d.ts.map +1 -1
  66. package/dist/lib/service-systemd-files.js +23 -0
  67. package/dist/lib/service-systemd-files.js.map +1 -1
  68. package/dist/lib/stack-journal.d.ts +16 -0
  69. package/dist/lib/stack-journal.d.ts.map +1 -1
  70. package/dist/lib/stack-journal.js +39 -8
  71. package/dist/lib/stack-journal.js.map +1 -1
  72. package/dist/lib/store-identity.d.ts +15 -0
  73. package/dist/lib/store-identity.d.ts.map +1 -0
  74. package/dist/lib/store-identity.js +30 -0
  75. package/dist/lib/store-identity.js.map +1 -0
  76. package/package.json +4 -4
@@ -1,8 +1,8 @@
1
- import { accessSync, constants, existsSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, lstatSync } 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, 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";
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, stackJournalPath, 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 };
@@ -166,7 +166,20 @@ export async function status(args) {
166
166
  const responder = await readResponderAxis(selected, preflight);
167
167
  const folder = blocked ? unreadFolder(root, blocked) : await readFolder(root, selected, values, responder.state, gaps, hints);
168
168
  const meshes = await readMeshes(cwd, selected, Boolean(values.all), gaps, offline, preflight);
169
- const target = await readSelected(selected, responder, folder, gaps, hints, 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);
170
183
  // The note under the heading sends here for this folder's own stack cut off with no recorded
171
184
  // outcome under another selection. A blocked one is named with its block under `This folder's
172
185
  // space` and its step first in Next, so that note is left out here, with its --json warning. One
@@ -174,11 +187,10 @@ export async function status(args) {
174
187
  const held = folderStack(folder, target);
175
188
  if (held && folder.stackBlock)
176
189
  withdrawOperationNote("FOLDER_SPACE_NOT_RUNNING", held.space);
177
- if (held && !folder.stackBlock && !folder.stackUnfinished && unsettledStackOperation(held.root, held.space))
190
+ if (!recovery && held && !folder.stackBlock && !folder.stackUnfinished && unsettledStackOperation(held.root, held.space))
178
191
  hints.push(`start it with \`cotal up\` in ${held.root}`);
179
192
  const services = readOwnedServices(selected, gaps);
180
193
  const bootAttempts = blocked ? { root, state: "partial", observedAt: new Date().toISOString(), unsettled: [], additive: [], unreadable: [], elsewhere: [] } : readBootAttempts(root, gaps);
181
- const maintenance = blocked ? { root, space: null, state: "unreadable", attemptId: null, next: null } : readMaintenance(root, gaps);
182
194
  const logged = selected.ok ? brokerStoreWrite(selected.target.root, selected.target.space) : undefined;
183
195
  // A store write failure holds until the broker restarts, so it is named before a full store.
184
196
  const full = selected.ok && target.resolved && target.storedBytes !== null && logged?.state !== "write-failed" ? storeFull(selected.target.root, selected.target.space, target.storedBytes) : undefined;
@@ -227,7 +239,7 @@ function folderReadProblem(root) {
227
239
  /** The folder section for a folder this account cannot read: every part of it is unread. */
228
240
  function unreadFolder(root, code) {
229
241
  const error = `${root} cannot be read by this account (${code})`;
230
- return { root, auth: { state: "unreadable", spaces: [], userAuth: false, error }, personas: { root: personaDir(root), default: "unreadable", demo: null, error }, launches: null, processes: null, processSpace: null, storedState: false, controllerStack: false, noManager: false, stackBlock: null };
242
+ return { root, auth: { state: "unreadable", spaces: [], userAuth: false, error }, personas: { root: personaDir(root), default: "unreadable", demo: null, error }, launches: null, processes: null, processSpace: null, storedState: false, controllerStack: false, stackJournal: "unreadable", noManager: false, stackBlock: null };
231
243
  }
232
244
  /** The folder's maintenance journal, read only. An unreadable one is reported, never taken for none. */
233
245
  function readMaintenance(root, gaps) {
@@ -537,6 +549,13 @@ async function readResponderAxis(selected, preflight) {
537
549
  }
538
550
  async function readFolder(root, selected, values, responder, gaps, hints) {
539
551
  const cmd = displayCmd();
552
+ let stackJournal;
553
+ try {
554
+ stackJournal = lstatSync(stackJournalPath(root), { throwIfNoEntry: false }) === undefined ? "absent" : "present";
555
+ }
556
+ catch {
557
+ stackJournal = "unreadable";
558
+ }
540
559
  const folder = (auth, processes = null, storedState = false, processSpace = null, controllerStack = false, noManager = false, stackBlock = null, stackUnfinished = null) => ({
541
560
  root,
542
561
  auth,
@@ -545,6 +564,7 @@ async function readFolder(root, selected, values, responder, gaps, hints) {
545
564
  processSpace,
546
565
  storedState,
547
566
  controllerStack,
567
+ stackJournal,
548
568
  noManager,
549
569
  stackBlock,
550
570
  stackUnfinished,
@@ -646,9 +666,9 @@ async function readFolder(root, selected, values, responder, gaps, hints) {
646
666
  controllerStack = false;
647
667
  // A journal no command can use is named with its way out, in the warnings and under Next; the
648
668
  // rest of the report still stands.
649
- if (isCommandFailure(error) && error.code === "SCHEMA_INVALID") {
669
+ if (isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED")) {
650
670
  unusable = true;
651
- gaps.add("STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
671
+ gaps.add(error.code === "PERMISSION_DENIED" ? "STACK_JOURNAL_UNREADABLE" : "STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
652
672
  if (error.remedy)
653
673
  hints.push(error.remedy);
654
674
  }
@@ -997,9 +1017,10 @@ async function readMeshes(cwd, selected, all, gaps, offline, preflight) {
997
1017
  catalog: m.origin === "catalog" ? { name: m.catalogName && m.catalogName !== m.space ? m.catalogName : null, fetchedAt: m.catalogFetchedAt ?? null, error: m.catalogError ?? null } : null,
998
1018
  })));
999
1019
  // The prepared spaces folder, or a record in it, that cannot be read is named the same way.
1000
- let currentPrepared = false, currentStopped = false;
1020
+ let currentPrepared = false, currentStopped = false, hasPrepared = false;
1001
1021
  try {
1002
1022
  const prepared = readPreparedSpaces();
1023
+ hasPrepared = prepared.spaces.length > 0;
1003
1024
  // Named with the warning a command that skips it gives, so a run that also skipped it says it once.
1004
1025
  for (const e of prepared.unusable) {
1005
1026
  const skipped = preparedRecordSkipped(e);
@@ -1019,6 +1040,7 @@ async function readMeshes(cwd, selected, all, gaps, offline, preflight) {
1019
1040
  current,
1020
1041
  currentRecorded: current === null || entries.some((m) => m.space === current),
1021
1042
  currentPrepared,
1043
+ hasPrepared,
1022
1044
  currentStopped,
1023
1045
  folder,
1024
1046
  unreadable,
@@ -1026,6 +1048,25 @@ async function readMeshes(cwd, selected, all, gaps, offline, preflight) {
1026
1048
  catalogs: preparedCatalogDiagnostics().map((r) => ({ idp: r.account.idpUrl, account: r.account.sub, state: r.state, error: r.state === "failed" ? (r.error ?? "refresh failed") : null })),
1027
1049
  };
1028
1050
  }
1051
+ /** Diagnose the current folder's store and a selected local stack independently of folder trust. */
1052
+ function readStackStoreWarnings(root, gaps, hints, space) {
1053
+ try {
1054
+ const journal = readStackJournal(root)?.journal;
1055
+ if (journal && (space === undefined || journal.space === space))
1056
+ for (const warning of stackStoreWarnings(journal))
1057
+ gaps.add(warning.code, warning.message, warning.target);
1058
+ }
1059
+ catch (error) {
1060
+ if (isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED")) {
1061
+ gaps.add(error.code === "PERMISSION_DENIED" ? "STACK_JOURNAL_UNREADABLE" : "STACK_JOURNAL_INVALID", error.remedy ? `${error.message}; ${error.remedy}` : error.message, { root });
1062
+ if (error.remedy)
1063
+ hints.push(error.remedy);
1064
+ }
1065
+ else if (!isCommandFailure(error) || error.code !== "CONFLICT")
1066
+ throw error;
1067
+ // A copied or moved journal is diagnosed by the folder and selection readers.
1068
+ }
1069
+ }
1029
1070
  /** The refusal of the stack journal at `root` when it is one no command can use, as `up` and `down`
1030
1071
  * refuse it. Undefined when it reads or is absent. */
1031
1072
  function unusableStackJournal(root) {
@@ -1034,7 +1075,7 @@ function unusableStackJournal(root) {
1034
1075
  return undefined;
1035
1076
  }
1036
1077
  catch (error) {
1037
- return isCommandFailure(error) && error.code === "SCHEMA_INVALID" ? error : undefined;
1078
+ return isCommandFailure(error) && (error.code === "SCHEMA_INVALID" || error.code === "PERMISSION_DENIED") ? error : undefined;
1038
1079
  }
1039
1080
  }
1040
1081
  /** The refusal of `space`'s service instance records when one is a record no command can use, as
@@ -1086,11 +1127,7 @@ async function readSelected(selected, responder, folder, gaps, hints, preflight)
1086
1127
  }
1087
1128
  if (e instanceof UnregisteredRoot) {
1088
1129
  const message = `no space is registered or prepared here for ${e.root}`;
1089
- // A bare `up` in a folder whose records name no space starts a space named after the folder,
1090
- // which then holds it, so the step that brings a forgotten space back is to prepare it again.
1091
- hints.push(e.namesNoSpace
1092
- ? `prepare a space with \`${cmd} space create <name> --root ${shellWord(e.root)}\`, or select one with \`${cmd} use <name>\``
1093
- : `run \`${cmd} up\` in ${e.root} to start one, or select a space with \`${cmd} use <name>\``);
1130
+ hints.push(`run \`${cmd} up\` in ${e.root} to start one, or select a space with \`${cmd} use <name>\``);
1094
1131
  return { resolved: false, code: "unregistered-root", message };
1095
1132
  }
1096
1133
  if (!isWorkspaceTargetError(e))
@@ -1157,6 +1194,16 @@ async function readSelected(selected, responder, folder, gaps, hints, preflight)
1157
1194
  spaceStreams: { state: "not-checked", root: null, stack: null },
1158
1195
  storedBytes: null,
1159
1196
  };
1197
+ // A moved project keeps its trust under its new path. Prefer that specific diagnosis when
1198
+ // this folder holds the records, otherwise report the missing stack root before any auth probe.
1199
+ const moved = target.mode === "auth" && !target.auth ? movedProject(target.space, target.root) : undefined;
1200
+ if (moved) {
1201
+ hints.push(moved.remedy);
1202
+ return { resolved: false, code: "moved-folder", message: moved.message };
1203
+ }
1204
+ const overridden = (target.source === "flag-server" || target.source === "flag-space-override") && target.recordedServer !== target.server;
1205
+ if (base.rootGone && !overridden)
1206
+ return { ...base, connection: "root-gone", snapshot: { state: "skipped", reason: "the stack's project folder was moved or deleted" } };
1160
1207
  if (target.mode === "user") {
1161
1208
  // Status never static-mints on a user-auth mesh (the flip). Everything here is offline
1162
1209
  // introspection — the login cache + the locally-readable ledger — plus, only when this
@@ -1189,13 +1236,6 @@ async function readSelected(selected, responder, folder, gaps, hints, preflight)
1189
1236
  return { ...result, snapshot: { state: "skipped", reason: "needs a signed-in, granted login" } };
1190
1237
  return { ...result, ...(await readSnapshot(() => userSnapshotEndpoint(target, stateDir), target.space, true, gaps)) };
1191
1238
  }
1192
- // A project folder moved after its stack started there keeps the seed under its new path, where a
1193
- // probe without it reads as authentication required. Name the move and the way back, as send does.
1194
- const moved = target.mode === "auth" && !target.auth ? movedProject(target.space, target.root) : undefined;
1195
- if (moved) {
1196
- hints.push(moved.remedy);
1197
- return { resolved: false, code: "moved-folder", message: moved.message };
1198
- }
1199
1239
  if (preflight === undefined)
1200
1240
  throw new Error("status: selected non-user target has no preflight observation");
1201
1241
  if (!preflight.ok) {
@@ -1579,7 +1619,8 @@ function extensionsCell(e) {
1579
1619
  * does not have is what `cotal setup` adds, which the applications line already counts. So an
1580
1620
  * application's own rows (the Claude plugin, Desktop and skills) appear only for an application that
1581
1621
  * IS set up, and only when they are not current; the broker executable appears when it is missing or
1582
- * refused; the dashboard, which is optional, appears when its record cannot be read. `--all` and
1622
+ * refused and local broker records make it relevant; the dashboard, which is optional, appears
1623
+ * when its record cannot be read. `--all` and
1583
1624
  * `--wide` print every row, and `--verbose` adds the installation paths ({@link machineSection}).
1584
1625
  *
1585
1626
  * It used to print one row per known connector whether or not it was set up, every extension label
@@ -1602,7 +1643,15 @@ function machineOverview(r, wide) {
1602
1643
  continue;
1603
1644
  rows.push([connector.name, stateCell(connector.missing.length ? `${connector.missing.join(", ")} not on PATH` : connector.requires.length ? "available on PATH" : "in-process", connector.missing.length ? "waiting" : "ok")]);
1604
1645
  }
1605
- if (wide || m.nats === "missing" || m.natsProblem)
1646
+ // A remote registration uses the broker elsewhere. Keep executable readiness in the full
1647
+ // diagnostics, without making a hosted client repair a binary it does not need to run.
1648
+ const localBroker = r.meshes.hasPrepared || r.meshes.currentPrepared || r.meshes.unreadable.length > 0 ||
1649
+ r.meshes.items.some((entry) => entry.origin !== "manual" && entry.origin !== "catalog") ||
1650
+ r.folder.controllerStack || r.folder.storedState || r.folder.auth.state !== "none" ||
1651
+ (r.folder.stackJournal !== undefined && r.folder.stackJournal !== "absent") ||
1652
+ r.folder.processes?.some((row) => row.name === "nats" && (row.pid !== null || row.state === "unreadable")) ||
1653
+ (!r.selected.resolved && (r.selected.code === "not-started" || r.selected.code === "unregistered-root" || r.selected.code === "unreadable-auth"));
1654
+ if (wide || (localBroker && (m.nats === "missing" || m.natsProblem)))
1606
1655
  rows.push(["Broker executable", stateCell(m.nats === "missing" ? "not installed" : brokerExecutable(m), m.nats === "missing" ? "waiting" : m.natsProblem ? "failed" : "ok")]);
1607
1656
  if (wide || (m.skills.state !== "current" && setUp.length))
1608
1657
  rows.push(["Agent skills", skillsCell(m.skills)]);
@@ -1621,15 +1670,11 @@ function machineOverview(r, wide) {
1621
1670
  ],
1622
1671
  };
1623
1672
  }
1624
- /** Whether `root` is the gone folder of the stack this machine still records for `space`. */
1625
- function stackRootGone(space, root) {
1626
- const gone = goneStackRoot(space);
1627
- return gone !== undefined && canonicalRoot(gone) === canonicalRoot(root);
1628
- }
1629
1673
  function connectionCell(s) {
1630
1674
  if (s.connection === "ok" || s.connection === "reachable")
1631
1675
  return stateCell("reachable", "ok");
1632
1676
  const states = {
1677
+ "root-gone": "project folder was moved or deleted",
1633
1678
  unreachable: "unreachable",
1634
1679
  "slow-link": "not answering",
1635
1680
  "broker-stopped": `broker process ${s.brokerPid} is stopped`,
@@ -1831,7 +1876,14 @@ function stackLines(folder) {
1831
1876
  function roleList(roles) {
1832
1877
  return roles.length > 1 ? `${roles.slice(0, -1).join(", ")} and ${roles.at(-1)}` : roles[0] ?? "";
1833
1878
  }
1879
+ /** Pre-commit backup claims block ordinary startup; ready cuts and committed restores do not. */
1880
+ function backupRecovery(maintenance) {
1881
+ return maintenance?.state === "claimed" || maintenance?.state === "restore-ready" ? maintenance.next : null;
1882
+ }
1834
1883
  function nextActions(r) {
1884
+ const recovery = backupRecovery(r.maintenance);
1885
+ if (recovery)
1886
+ return { title: "Next", blocks: [...new Set([recovery, ...r.hints])].map((hint) => ({ kind: "lines", lines: [cell(hint, "hint")] })) };
1835
1887
  const cmd = displayCmd();
1836
1888
  const items = [];
1837
1889
  const extra = [];