@mutmutco/hub 4.1.6 → 4.1.8

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 (2) hide show
  1. package/dist/index.cjs +406 -258
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  acquireSharedDocLock: () => acquireSharedDocLock,
25
25
  buildWatcherCommand: () => buildWatcherCommand,
26
+ clearLaunchTarget: () => clearLaunchTarget,
26
27
  clearPending: () => clearPending,
27
28
  enumerateSurfaces: () => enumerateSurfaces,
28
29
  failConsequence: () => failConsequence,
@@ -38,13 +39,19 @@ __export(index_exports, {
38
39
  lockShaped: () => lockShaped,
39
40
  main: () => main,
40
41
  noteBusyDefer: () => noteBusyDefer,
42
+ noteLaunchStaged: () => noteLaunchStaged,
41
43
  npmStringField: () => npmStringField,
42
44
  ownVersion: () => ownVersion,
43
45
  parseNpmDistIntegrity: () => parseNpmDistIntegrity,
44
46
  pendingPath: () => pendingPath,
45
47
  pidAlive: () => pidAlive,
48
+ probeCursorLaunchStaging: () => probeCursorLaunchStaging,
46
49
  probeHermesInstallation: () => probeHermesInstallation,
50
+ probeHermesLaunchStaging: () => probeHermesLaunchStaging,
51
+ probeKimiLaunchStaging: () => probeKimiLaunchStaging,
52
+ probeLaunchTargetVersion: () => probeLaunchTargetVersion,
47
53
  progressiveRow: () => progressiveRow,
54
+ readLaunchTarget: () => readLaunchTarget,
48
55
  readPending: () => readPending,
49
56
  reconcileCursorUserHooks: () => reconcileCursorUserHooks,
50
57
  reconcileKimiInstalledPlugin: () => reconcileKimiInstalledPlugin,
@@ -64,11 +71,12 @@ __export(index_exports, {
64
71
  watcherLockPath: () => watcherLockPath,
65
72
  withSharedDocLock: () => withSharedDocLock,
66
73
  wrapWords: () => wrapWords,
74
+ writeLaunchTarget: () => writeLaunchTarget,
67
75
  writePending: () => writePending
68
76
  });
69
77
  module.exports = __toCommonJS(index_exports);
70
- var import_node_fs18 = require("node:fs");
71
- var import_node_path14 = require("node:path");
78
+ var import_node_fs19 = require("node:fs");
79
+ var import_node_path15 = require("node:path");
72
80
 
73
81
  // src/state.ts
74
82
  var import_node_fs = require("node:fs");
@@ -1008,8 +1016,8 @@ function kiloArm(options) {
1008
1016
 
1009
1017
  // src/arm-kimi.ts
1010
1018
  var import_node_child_process7 = require("node:child_process");
1011
- var import_node_fs11 = require("node:fs");
1012
- var import_node_path8 = require("node:path");
1019
+ var import_node_fs12 = require("node:fs");
1020
+ var import_node_path9 = require("node:path");
1013
1021
 
1014
1022
  // src/compat.ts
1015
1023
  var import_node_fs9 = require("node:fs");
@@ -1022,6 +1030,55 @@ function readManifestCompat(manifestPath) {
1022
1030
  }
1023
1031
  }
1024
1032
 
1033
+ // src/launch-staging.ts
1034
+ var import_node_fs10 = require("node:fs");
1035
+ var import_node_path8 = require("node:path");
1036
+ function readLaunchTarget(path) {
1037
+ try {
1038
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
1039
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1040
+ const row = parsed;
1041
+ if (typeof row.surface !== "string" || typeof row.target !== "string" || typeof row.since !== "string" || typeof row.incoming !== "string") return null;
1042
+ return row;
1043
+ } catch {
1044
+ return null;
1045
+ }
1046
+ }
1047
+ function writeLaunchTarget(path, record) {
1048
+ (0, import_node_fs10.mkdirSync)((0, import_node_path8.join)(path, ".."), { recursive: true });
1049
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1050
+ (0, import_node_fs10.writeFileSync)(tmp, `${JSON.stringify(record, null, 2)}
1051
+ `, "utf8");
1052
+ (0, import_node_fs10.renameSync)(tmp, path);
1053
+ }
1054
+ function clearLaunchTarget(path) {
1055
+ try {
1056
+ (0, import_node_fs10.rmSync)(path, { force: true });
1057
+ } catch {
1058
+ }
1059
+ }
1060
+ function noteLaunchStaged(paths, env, options, seam = {}) {
1061
+ writeLaunchTarget(options.launchTargetPath, {
1062
+ surface: options.surface,
1063
+ target: options.target,
1064
+ since: (/* @__PURE__ */ new Date()).toISOString(),
1065
+ incoming: options.incoming
1066
+ });
1067
+ return noteBusyDefer(paths, env, {
1068
+ surface: options.surface,
1069
+ target: options.target,
1070
+ reason: options.reason,
1071
+ images: options.images,
1072
+ dirs: options.dirs
1073
+ }, seam);
1074
+ }
1075
+ function probeLaunchTargetVersion(launchTargetPath, versionAt) {
1076
+ const record = readLaunchTarget(launchTargetPath);
1077
+ if (!record || !(0, import_node_fs10.existsSync)(record.incoming)) return null;
1078
+ const version = versionAt(record.incoming);
1079
+ return version === record.target ? record.target : null;
1080
+ }
1081
+
1025
1082
  // src/kimi-registration.ts
1026
1083
  function validateKimiInstalledPlugin(document, pluginRoot, id) {
1027
1084
  if (!document || typeof document !== "object" || Array.isArray(document)) return "malformed-document";
@@ -1038,7 +1095,7 @@ function validateKimiInstalledPlugin(document, pluginRoot, id) {
1038
1095
  }
1039
1096
 
1040
1097
  // src/host-doc-lock.ts
1041
- var import_node_fs10 = require("node:fs");
1098
+ var import_node_fs11 = require("node:fs");
1042
1099
  var import_node_os4 = require("node:os");
1043
1100
  var SHARED_DOC_LOCK_STALE_MS = 3e4;
1044
1101
  var SHARED_DOC_LOCK_ATTEMPTS = 50;
@@ -1052,7 +1109,7 @@ function lockContent(options) {
1052
1109
  }
1053
1110
  function readHolder(lockPath) {
1054
1111
  try {
1055
- const parsed = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf8"));
1112
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(lockPath, "utf8"));
1056
1113
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1057
1114
  return parsed;
1058
1115
  } catch {
@@ -1064,7 +1121,7 @@ function lockAgeMs(lockPath, now) {
1064
1121
  const declared = typeof holder?.acquiredAt === "string" ? Date.parse(holder.acquiredAt) : Number.NaN;
1065
1122
  if (!Number.isNaN(declared)) return Math.max(0, now - declared);
1066
1123
  try {
1067
- return Math.max(0, now - (0, import_node_fs10.statSync)(lockPath).mtimeMs);
1124
+ return Math.max(0, now - (0, import_node_fs11.statSync)(lockPath).mtimeMs);
1068
1125
  } catch {
1069
1126
  return 0;
1070
1127
  }
@@ -1087,9 +1144,9 @@ function acquireSharedDocLock(documentPath, options) {
1087
1144
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1088
1145
  let fd;
1089
1146
  try {
1090
- fd = (0, import_node_fs10.openSync)(lockPath, "wx");
1091
- (0, import_node_fs10.writeSync)(fd, content);
1092
- (0, import_node_fs10.closeSync)(fd);
1147
+ fd = (0, import_node_fs11.openSync)(lockPath, "wx");
1148
+ (0, import_node_fs11.writeSync)(fd, content);
1149
+ (0, import_node_fs11.closeSync)(fd);
1093
1150
  return {
1094
1151
  documentPath,
1095
1152
  lockPath,
@@ -1102,11 +1159,11 @@ function acquireSharedDocLock(documentPath, options) {
1102
1159
  } catch (error) {
1103
1160
  if (fd !== void 0) {
1104
1161
  try {
1105
- (0, import_node_fs10.closeSync)(fd);
1162
+ (0, import_node_fs11.closeSync)(fd);
1106
1163
  } catch {
1107
1164
  }
1108
1165
  try {
1109
- (0, import_node_fs10.unlinkSync)(lockPath);
1166
+ (0, import_node_fs11.unlinkSync)(lockPath);
1110
1167
  } catch {
1111
1168
  }
1112
1169
  }
@@ -1115,12 +1172,12 @@ function acquireSharedDocLock(documentPath, options) {
1115
1172
  if (lockAgeMs(lockPath, Date.now()) >= staleAfterMs) {
1116
1173
  const aside = `${lockPath}.stale-${Date.now()}`;
1117
1174
  try {
1118
- (0, import_node_fs10.renameSync)(lockPath, aside);
1175
+ (0, import_node_fs11.renameSync)(lockPath, aside);
1119
1176
  } catch {
1120
1177
  continue;
1121
1178
  }
1122
1179
  try {
1123
- (0, import_node_fs10.rmSync)(aside, { force: true });
1180
+ (0, import_node_fs11.rmSync)(aside, { force: true });
1124
1181
  } catch {
1125
1182
  }
1126
1183
  continue;
@@ -1133,7 +1190,7 @@ function releaseSharedDocLock(handle) {
1133
1190
  try {
1134
1191
  const holder = readHolder(handle.lockPath);
1135
1192
  if (holder && holder.owner === handle.owner && Number(holder.pid) === handle.pid) {
1136
- (0, import_node_fs10.unlinkSync)(handle.lockPath);
1193
+ (0, import_node_fs11.unlinkSync)(handle.lockPath);
1137
1194
  }
1138
1195
  } catch {
1139
1196
  }
@@ -1151,11 +1208,11 @@ function withSharedDocLock(documentPath, options, fn) {
1151
1208
  var KIMI_PACKAGE = "@mutmutco/kimi-plugin";
1152
1209
  var TREE_MARKERS = [".kimi-plugin/plugin.json", "skills/mmi/SKILL.md", "scripts/hook-run.mjs"];
1153
1210
  function treeHealthy(root) {
1154
- return TREE_MARKERS.every((rel) => fileExists((0, import_node_path8.join)(root, ...rel.split("/"))));
1211
+ return TREE_MARKERS.every((rel) => fileExists((0, import_node_path9.join)(root, ...rel.split("/"))));
1155
1212
  }
1156
1213
  function markerVersion(root) {
1157
1214
  try {
1158
- const version = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path8.join)(root, ".kimi-plugin", "plugin.json"), "utf8")).version;
1215
+ const version = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(root, ".kimi-plugin", "plugin.json"), "utf8")).version;
1159
1216
  return typeof version === "string" ? version : null;
1160
1217
  } catch {
1161
1218
  return null;
@@ -1163,7 +1220,7 @@ function markerVersion(root) {
1163
1220
  }
1164
1221
  function packageVersion(root) {
1165
1222
  try {
1166
- const version = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path8.join)(root, "package.json"), "utf8")).version;
1223
+ const version = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(root, "package.json"), "utf8")).version;
1167
1224
  return typeof version === "string" ? version : null;
1168
1225
  } catch {
1169
1226
  return null;
@@ -1171,18 +1228,18 @@ function packageVersion(root) {
1171
1228
  }
1172
1229
  var KIMI_REGISTRATION_LOCK_OWNER = "mmi-hub:kimi-registration";
1173
1230
  function reconcileKimiInstalledPlugin(kimiHomeDir, pluginRoot, id, lock = {}) {
1174
- const path = (0, import_node_path8.join)(kimiHomeDir, "plugins", "installed.json");
1175
- (0, import_node_fs11.mkdirSync)((0, import_node_path8.join)(kimiHomeDir, "plugins"), { recursive: true });
1231
+ const path = (0, import_node_path9.join)(kimiHomeDir, "plugins", "installed.json");
1232
+ (0, import_node_fs12.mkdirSync)((0, import_node_path9.join)(kimiHomeDir, "plugins"), { recursive: true });
1176
1233
  return withSharedDocLock(path, { owner: KIMI_REGISTRATION_LOCK_OWNER, ...lock }, () => mergeKimiInstalledPlugin(path, pluginRoot, id));
1177
1234
  }
1178
1235
  function mergeKimiInstalledPlugin(path, pluginRoot, id) {
1179
1236
  const now = (/* @__PURE__ */ new Date()).toISOString();
1180
1237
  const fresh = { id, root: pluginRoot, enabled: true, installedAt: now, updatedAt: now, originalSource: pluginRoot, source: "local-path" };
1181
1238
  let document = { version: 1, plugins: [] };
1182
- if ((0, import_node_fs11.existsSync)(path)) {
1239
+ if ((0, import_node_fs12.existsSync)(path)) {
1183
1240
  let parsed;
1184
1241
  try {
1185
- parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
1242
+ parsed = JSON.parse((0, import_node_fs12.readFileSync)(path, "utf8"));
1186
1243
  } catch {
1187
1244
  return "conflict";
1188
1245
  }
@@ -1210,13 +1267,13 @@ function mergeKimiInstalledPlugin(path, pluginRoot, id) {
1210
1267
  plugins[index] = { ...next, updatedAt: now };
1211
1268
  }
1212
1269
  const tmp = `${path}.tmp-${process.pid}`;
1213
- (0, import_node_fs11.writeFileSync)(tmp, `${JSON.stringify({ ...document, version: 1, plugins }, null, 2)}
1270
+ (0, import_node_fs12.writeFileSync)(tmp, `${JSON.stringify({ ...document, version: 1, plugins }, null, 2)}
1214
1271
  `, "utf8");
1215
- (0, import_node_fs11.renameSync)(tmp, path);
1272
+ (0, import_node_fs12.renameSync)(tmp, path);
1216
1273
  return "updated";
1217
1274
  }
1218
1275
  function probeKimiInstallation(env = process.env) {
1219
- const location = (0, import_node_path8.join)(kimiHome(env), "plugins", "managed", "mmi");
1276
+ const location = (0, import_node_path9.join)(kimiHome(env), "plugins", "managed", "mmi");
1220
1277
  const version = markerVersion(location);
1221
1278
  return { version, location, detail: version ? "installed Kimi plugin manifest" : "installed Kimi plugin manifest was unreadable" };
1222
1279
  }
@@ -1224,14 +1281,14 @@ function tail2(output) {
1224
1281
  return output.split("\n").filter(Boolean).slice(-3).join(" | ");
1225
1282
  }
1226
1283
  function scratchRoot(env) {
1227
- return (0, import_node_path8.join)(kimiHome(env), ".mmi-updater");
1284
+ return (0, import_node_path9.join)(kimiHome(env), ".mmi-updater");
1228
1285
  }
1229
1286
  function pruneScratchDir(root, keep) {
1230
1287
  try {
1231
- for (const name of (0, import_node_fs11.readdirSync)(root)) {
1288
+ for (const name of (0, import_node_fs12.readdirSync)(root)) {
1232
1289
  if (name === keep) continue;
1233
1290
  try {
1234
- (0, import_node_fs11.rmSync)((0, import_node_path8.join)(root, name), { recursive: true, force: true });
1291
+ (0, import_node_fs12.rmSync)((0, import_node_path9.join)(root, name), { recursive: true, force: true });
1235
1292
  } catch {
1236
1293
  }
1237
1294
  }
@@ -1257,31 +1314,32 @@ function kimiHostRunning(env) {
1257
1314
  }
1258
1315
  function restoreQuarantine(quarantine, live) {
1259
1316
  try {
1260
- (0, import_node_fs11.renameSync)(quarantine, live);
1317
+ (0, import_node_fs12.renameSync)(quarantine, live);
1261
1318
  } catch (error) {
1262
1319
  return { restored: false, detail: `${error?.message ?? error}` };
1263
1320
  }
1264
- if (!(0, import_node_fs11.existsSync)(live)) return { restored: false, detail: `${live} is still absent after the restore rename` };
1321
+ if (!(0, import_node_fs12.existsSync)(live)) return { restored: false, detail: `${live} is still absent after the restore rename` };
1265
1322
  return { restored: true, detail: `marker ${markerVersion(live) ?? "unreadable"}` };
1266
1323
  }
1267
1324
  function kimiArm(options) {
1268
1325
  const env = options.env ?? process.env;
1269
1326
  const { target, dryRun, paths } = options;
1270
- const result = (from, to, verdict, detail) => ({
1327
+ const result = (from, to, verdict, detail, launchStaged = false) => ({
1271
1328
  surface: "kimi",
1272
1329
  from,
1273
1330
  to,
1274
1331
  verdict,
1275
1332
  detail,
1333
+ launchStaged: launchStaged || void 0,
1276
1334
  // C11: the constraint of the tree LEFT at `live` — read at result time, so ok carries the new
1277
1335
  // manifest's declaration and defer/fail carry the previous tree's (#4973 reconsult ruling).
1278
- compat: readManifestCompat((0, import_node_path8.join)(live, ".kimi-plugin", "plugin.json"))
1336
+ compat: readManifestCompat((0, import_node_path9.join)(live, ".kimi-plugin", "plugin.json"))
1279
1337
  });
1280
- const managedParent = (0, import_node_path8.join)(kimiHome(env), "plugins", "managed");
1281
- const live = (0, import_node_path8.join)(managedParent, "mmi");
1338
+ const managedParent = (0, import_node_path9.join)(kimiHome(env), "plugins", "managed");
1339
+ const live = (0, import_node_path9.join)(managedParent, "mmi");
1282
1340
  const installed = markerVersion(live);
1283
1341
  const ensureRegistered = (detail) => {
1284
- if (!(0, import_node_fs11.existsSync)(live) || !treeHealthy(live)) return { detail };
1342
+ if (!(0, import_node_fs12.existsSync)(live) || !treeHealthy(live)) return { detail };
1285
1343
  if (dryRun) return { detail: `${detail}; installed.json untouched (dry-run)` };
1286
1344
  let registration;
1287
1345
  try {
@@ -1310,11 +1368,11 @@ function kimiArm(options) {
1310
1368
  if (dryRun) {
1311
1369
  return result(installed, target, "ok", `dry-run: would converge ${installed ?? "absent"} -> ${target} (tarball generation switch while Kimi is absent)`);
1312
1370
  }
1313
- const stageRoot = (0, import_node_path8.join)(paths.staging, "kimi", target);
1314
- const staged = (0, import_node_path8.join)(stageRoot, "node_modules", ...KIMI_PACKAGE.split("/"));
1371
+ const stageRoot = (0, import_node_path9.join)(paths.staging, "kimi", target);
1372
+ const staged = (0, import_node_path9.join)(stageRoot, "node_modules", ...KIMI_PACKAGE.split("/"));
1315
1373
  if (!treeHealthy(staged)) {
1316
- (0, import_node_fs11.mkdirSync)(stageRoot, { recursive: true });
1317
- (0, import_node_fs11.writeFileSync)((0, import_node_path8.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1374
+ (0, import_node_fs12.mkdirSync)(stageRoot, { recursive: true });
1375
+ (0, import_node_fs12.writeFileSync)((0, import_node_path9.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
1318
1376
  const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${KIMI_PACKAGE}@${target}`], env);
1319
1377
  if (install.status !== 0) {
1320
1378
  return result(installed, target, "defer", `staging install failed: ${tail2(install.stderr || install.stdout)}`);
@@ -1331,33 +1389,57 @@ function kimiArm(options) {
1331
1389
  if (!treeHealthy(staged)) {
1332
1390
  return result(installed, target, "defer", `staged tree is missing one of ${TREE_MARKERS.join(", ")}`);
1333
1391
  }
1334
- const quiescence = kimiHostRunning(env);
1335
- if (quiescence.running) {
1336
- const pending = options.dryRun ? "" : ` ${noteBusyDefer(options.paths, env, { surface: "kimi", target, reason: quiescence.evidence, dirs: ["kimi"] })}`;
1337
- return result(installed, target, "defer", `Kimi may be running \u2014 the managed tree is switched only while the host is absent: ${quiescence.evidence}${pending ? ` \u2014${pending}` : ""}`);
1338
- }
1339
1392
  const scratch = scratchRoot(env);
1340
- const incomingRoot = (0, import_node_path8.join)(scratch, "incoming");
1341
- const incoming = (0, import_node_path8.join)(incomingRoot, target);
1342
- const quarantineRoot = (0, import_node_path8.join)(scratch, "quarantine");
1343
- const rejectedRoot = (0, import_node_path8.join)(scratch, "rejected");
1344
- try {
1345
- (0, import_node_fs11.mkdirSync)(managedParent, { recursive: true });
1346
- (0, import_node_fs11.mkdirSync)(quarantineRoot, { recursive: true });
1347
- (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1348
- (0, import_node_fs11.cpSync)(staged, incoming, { recursive: true });
1349
- } catch (error) {
1350
- return result(installed, target, "defer", `cannot place the incoming generation under ${scratch}: ${error?.message ?? error}`);
1393
+ const launchTargetPath = (0, import_node_path9.join)(scratch, "launch-target.json");
1394
+ const incomingRoot = (0, import_node_path9.join)(scratch, "incoming");
1395
+ const incoming = (0, import_node_path9.join)(incomingRoot, target);
1396
+ const quarantineRoot = (0, import_node_path9.join)(scratch, "quarantine");
1397
+ const rejectedRoot = (0, import_node_path9.join)(scratch, "rejected");
1398
+ const priorLaunch = readLaunchTarget(launchTargetPath);
1399
+ const reuseIncoming = priorLaunch?.target === target && priorLaunch.incoming === incoming && markerVersion(incoming) === target && treeHealthy(incoming);
1400
+ if (!reuseIncoming) {
1401
+ try {
1402
+ (0, import_node_fs12.mkdirSync)(managedParent, { recursive: true });
1403
+ (0, import_node_fs12.mkdirSync)(quarantineRoot, { recursive: true });
1404
+ (0, import_node_fs12.rmSync)(incomingRoot, { recursive: true, force: true });
1405
+ (0, import_node_fs12.cpSync)(staged, incoming, { recursive: true });
1406
+ } catch (error) {
1407
+ return result(installed, target, "defer", `cannot place the incoming generation under ${scratch}: ${error?.message ?? error}`);
1408
+ }
1409
+ if (markerVersion(incoming) !== target || !treeHealthy(incoming)) {
1410
+ return result(installed, target, "defer", `incoming generation verifies as ${markerVersion(incoming) ?? "unreadable"}, expected ${target}`);
1411
+ }
1351
1412
  }
1352
- if (markerVersion(incoming) !== target || !treeHealthy(incoming)) {
1353
- return result(installed, target, "defer", `incoming generation verifies as ${markerVersion(incoming) ?? "unreadable"}, expected ${target}`);
1413
+ const quiescence = kimiHostRunning(env);
1414
+ if (quiescence.running) {
1415
+ const pending = noteLaunchStaged(paths, env, {
1416
+ surface: "kimi",
1417
+ target,
1418
+ reason: quiescence.evidence,
1419
+ launchTargetPath,
1420
+ incoming,
1421
+ dirs: ["kimi"]
1422
+ });
1423
+ return result(installed, target, "ok", `staged ${target} for next launch while Kimi is running (${quiescence.evidence}) \u2014 ${pending}`, true);
1424
+ }
1425
+ const started = kimiHostRunning(env);
1426
+ if (started.running) {
1427
+ const pending = noteLaunchStaged(paths, env, {
1428
+ surface: "kimi",
1429
+ target,
1430
+ reason: started.evidence,
1431
+ launchTargetPath,
1432
+ incoming,
1433
+ dirs: ["kimi"]
1434
+ });
1435
+ return result(installed, target, "ok", `staged ${target} for next launch \u2014 Kimi started during staging (${started.evidence}) \u2014 ${pending}`, true);
1354
1436
  }
1355
- const hadLive = (0, import_node_fs11.existsSync)(live);
1437
+ const hadLive = (0, import_node_fs12.existsSync)(live);
1356
1438
  const generation = `${installed ?? "unknown"}-${Date.now()}`;
1357
- const quarantine = (0, import_node_path8.join)(quarantineRoot, generation);
1439
+ const quarantine = (0, import_node_path9.join)(quarantineRoot, generation);
1358
1440
  if (hadLive) {
1359
1441
  try {
1360
- (0, import_node_fs11.renameSync)(live, quarantine);
1442
+ (0, import_node_fs12.renameSync)(live, quarantine);
1361
1443
  } catch (error) {
1362
1444
  const reason = String(error?.message ?? error);
1363
1445
  const pending = lockShaped(reason) ? ` \u2014 ${noteBusyDefer(options.paths, env, { surface: "kimi", target, reason, dirs: ["kimi"] })}` : "";
@@ -1365,7 +1447,7 @@ function kimiArm(options) {
1365
1447
  }
1366
1448
  }
1367
1449
  try {
1368
- (0, import_node_fs11.renameSync)(incoming, live);
1450
+ (0, import_node_fs12.renameSync)(incoming, live);
1369
1451
  } catch (error) {
1370
1452
  const failed = `cannot switch the new generation in: ${error?.message ?? error}`;
1371
1453
  if (!hadLive) {
@@ -1380,33 +1462,37 @@ function kimiArm(options) {
1380
1462
  const observed = `post-switch marker is ${switched ?? "unreadable"}, expected ${target}`;
1381
1463
  const rejectedName = `${target}-${Date.now()}`;
1382
1464
  try {
1383
- (0, import_node_fs11.mkdirSync)(rejectedRoot, { recursive: true });
1384
- (0, import_node_fs11.renameSync)(live, (0, import_node_path8.join)(rejectedRoot, rejectedName));
1465
+ (0, import_node_fs12.mkdirSync)(rejectedRoot, { recursive: true });
1466
+ (0, import_node_fs12.renameSync)(live, (0, import_node_path9.join)(rejectedRoot, rejectedName));
1385
1467
  } catch (error) {
1386
1468
  return result(installed, switched ?? "unknown", "fail", `${observed} \u2014 the unverified tree could NOT be moved aside (${error?.message ?? error}) and is STILL LIVE at ${live}${hadLive ? `; the previous generation is intact at ${quarantine}` : ""}`);
1387
1469
  }
1388
1470
  pruneScratchDir(rejectedRoot, rejectedName);
1389
1471
  if (!hadLive) {
1390
- return result(installed, "none", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path8.join)(rejectedRoot, rejectedName)}; ${live} is absent (there was no previous generation to restore)`);
1472
+ return result(installed, "none", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path9.join)(rejectedRoot, rejectedName)}; ${live} is absent (there was no previous generation to restore)`);
1391
1473
  }
1392
1474
  const restore = restoreQuarantine(quarantine, live);
1393
- return restore.restored ? result(installed, installed ?? "unknown", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path8.join)(rejectedRoot, rejectedName)}; rolled back to ${installed ?? "the previous generation"} and read back (${restore.detail})`) : result(installed, "none", "fail", `${observed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1475
+ return restore.restored ? result(installed, installed ?? "unknown", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path9.join)(rejectedRoot, rejectedName)}; rolled back to ${installed ?? "the previous generation"} and read back (${restore.detail})`) : result(installed, "none", "fail", `${observed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
1394
1476
  }
1395
1477
  pruneScratchDir(quarantineRoot, hadLive ? generation : null);
1396
1478
  pruneScratchDir(rejectedRoot, null);
1397
1479
  try {
1398
- (0, import_node_fs11.rmSync)(incomingRoot, { recursive: true, force: true });
1480
+ (0, import_node_fs12.rmSync)(incomingRoot, { recursive: true, force: true });
1399
1481
  } catch {
1400
1482
  }
1483
+ clearLaunchTarget(launchTargetPath);
1401
1484
  const reg = ensureRegistered(`converged ${installed ?? "absent"} -> ${target} (staged, verified, switched${hadLive ? `; previous generation quarantined at ${quarantine}` : ""})`);
1402
1485
  return result(installed, target, reg.fail ? "fail" : "ok", reg.detail);
1403
1486
  }
1487
+ function probeKimiLaunchStaging(env = process.env) {
1488
+ return probeLaunchTargetVersion((0, import_node_path9.join)(scratchRoot(env), "launch-target.json"), markerVersion);
1489
+ }
1404
1490
 
1405
1491
  // src/arm-cursor.ts
1406
- var import_node_fs12 = require("node:fs");
1492
+ var import_node_fs13 = require("node:fs");
1407
1493
  var import_node_child_process8 = require("node:child_process");
1408
1494
  var import_node_os5 = require("node:os");
1409
- var import_node_path9 = require("node:path");
1495
+ var import_node_path10 = require("node:path");
1410
1496
  var PACKAGE = "@mutmutco/cursor-plugin";
1411
1497
  var MANIFEST = ".cursor-plugin/plugin.json";
1412
1498
  var USER_HOOKS_FILE = "hooks.json";
@@ -1415,14 +1501,14 @@ var KEEP_GENERATIONS = 2;
1415
1501
  var STRANDED_TMP_AGE_MS = 24 * 60 * 6e4;
1416
1502
  var TREE_FILES = [MANIFEST, "skills/mmi/SKILL.md", "hooks/cursor-hooks.json", "scripts/hook-run.mjs", "scripts/hook-policy.mjs"];
1417
1503
  function cursorPluginsRoot() {
1418
- return (0, import_node_path9.join)((0, import_node_os5.homedir)(), ".cursor", "plugins");
1504
+ return (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".cursor", "plugins");
1419
1505
  }
1420
1506
  function treeHealthy2(root) {
1421
- return TREE_FILES.every((rel) => (0, import_node_fs12.existsSync)((0, import_node_path9.join)(root, ...rel.split("/"))));
1507
+ return TREE_FILES.every((rel) => (0, import_node_fs13.existsSync)((0, import_node_path10.join)(root, ...rel.split("/"))));
1422
1508
  }
1423
1509
  function readJson(root, rel) {
1424
1510
  try {
1425
- return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(root, ...rel.split("/")), "utf8"));
1511
+ return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(root, ...rel.split("/")), "utf8"));
1426
1512
  } catch {
1427
1513
  return null;
1428
1514
  }
@@ -1431,7 +1517,7 @@ function manifest(root) {
1431
1517
  return readJson(root, MANIFEST);
1432
1518
  }
1433
1519
  function probeCursorInstallation() {
1434
- const location = (0, import_node_path9.join)(cursorPluginsRoot(), "local", "mmi");
1520
+ const location = (0, import_node_path10.join)(cursorPluginsRoot(), "local", "mmi");
1435
1521
  const version = manifest(location)?.version ?? null;
1436
1522
  return { version, location, detail: version ? "installed Cursor plugin manifest" : "installed Cursor plugin manifest was unreadable" };
1437
1523
  }
@@ -1446,7 +1532,7 @@ function message(error) {
1446
1532
  }
1447
1533
  function discard(path) {
1448
1534
  try {
1449
- (0, import_node_fs12.rmSync)(path, { recursive: true, force: true });
1535
+ (0, import_node_fs13.rmSync)(path, { recursive: true, force: true });
1450
1536
  } catch {
1451
1537
  }
1452
1538
  }
@@ -1474,7 +1560,7 @@ function cursorHostEvidence(env) {
1474
1560
  return null;
1475
1561
  }
1476
1562
  function resolvedPluginRoot(root) {
1477
- return (0, import_node_path9.join)(root).replace(/\\/g, "/");
1563
+ return (0, import_node_path10.join)(root).replace(/\\/g, "/");
1478
1564
  }
1479
1565
  function isMmiEntry(entry, pluginRoot) {
1480
1566
  return typeof entry === "object" && entry !== null && typeof entry.command === "string" && entry.command.includes(resolvedPluginRoot(pluginRoot));
@@ -1484,7 +1570,7 @@ function resolveCommand(command, pluginRoot) {
1484
1570
  }
1485
1571
  function desiredUserHooks(pluginRoot) {
1486
1572
  try {
1487
- const doc = JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path9.join)(pluginRoot, ...PLUGIN_HOOKS_REL), "utf8"));
1573
+ const doc = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(pluginRoot, ...PLUGIN_HOOKS_REL), "utf8"));
1488
1574
  const hooks = doc.hooks;
1489
1575
  if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) return null;
1490
1576
  const desired = /* @__PURE__ */ new Map();
@@ -1521,17 +1607,17 @@ var CURSOR_HOOKS_LOCK_OWNER = "mmi-hub:cursor-hooks";
1521
1607
  function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1522
1608
  const desired = desiredUserHooks(pluginRoot);
1523
1609
  if (!desired) return "source-unreadable";
1524
- const path = (0, import_node_path9.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1610
+ const path = (0, import_node_path10.join)(homeDir, ".cursor", USER_HOOKS_FILE);
1525
1611
  return withSharedDocLock(path, { owner: CURSOR_HOOKS_LOCK_OWNER, ...lock }, () => {
1526
- if (!(0, import_node_fs12.existsSync)(path)) {
1612
+ if (!(0, import_node_fs13.existsSync)(path)) {
1527
1613
  const fresh = `${path}.tmp-${process.pid}`;
1528
- (0, import_node_fs12.writeFileSync)(fresh, JSON.stringify({ version: 1, hooks: Object.fromEntries(desired) }, null, 2) + "\n", "utf8");
1529
- (0, import_node_fs12.renameSync)(fresh, path);
1614
+ (0, import_node_fs13.writeFileSync)(fresh, JSON.stringify({ version: 1, hooks: Object.fromEntries(desired) }, null, 2) + "\n", "utf8");
1615
+ (0, import_node_fs13.renameSync)(fresh, path);
1530
1616
  return "updated";
1531
1617
  }
1532
1618
  let existing;
1533
1619
  try {
1534
- existing = JSON.parse((0, import_node_fs12.readFileSync)(path, "utf8"));
1620
+ existing = JSON.parse((0, import_node_fs13.readFileSync)(path, "utf8"));
1535
1621
  } catch {
1536
1622
  return "user-conflict";
1537
1623
  }
@@ -1539,8 +1625,8 @@ function reconcileCursorUserHooks(pluginRoot, homeDir, lock = {}) {
1539
1625
  if (!merged) return "user-conflict";
1540
1626
  if (JSON.stringify(merged) === JSON.stringify(existing)) return "current";
1541
1627
  const tmp = `${path}.tmp-${process.pid}`;
1542
- (0, import_node_fs12.writeFileSync)(tmp, JSON.stringify(merged, null, 2) + "\n", "utf8");
1543
- (0, import_node_fs12.renameSync)(tmp, path);
1628
+ (0, import_node_fs13.writeFileSync)(tmp, JSON.stringify(merged, null, 2) + "\n", "utf8");
1629
+ (0, import_node_fs13.renameSync)(tmp, path);
1544
1630
  return "updated";
1545
1631
  });
1546
1632
  }
@@ -1555,7 +1641,7 @@ function userHooksDetail(pluginRoot, homeDir) {
1555
1641
  case "updated":
1556
1642
  return "; user hooks bridged into ~/.cursor/hooks.json";
1557
1643
  case "user-conflict":
1558
- return `; USER HOOKS CONFLICT at ${(0, import_node_path9.join)(homeDir, ".cursor", USER_HOOKS_FILE)} \u2014 Cursor gates are NOT bridged; fix or remove that file and reconcile`;
1644
+ return `; USER HOOKS CONFLICT at ${(0, import_node_path10.join)(homeDir, ".cursor", USER_HOOKS_FILE)} \u2014 Cursor gates are NOT bridged; fix or remove that file and reconcile`;
1559
1645
  case "source-unreadable":
1560
1646
  return "; user hooks source unreadable \u2014 Cursor gates are NOT bridged";
1561
1647
  default:
@@ -1563,35 +1649,35 @@ function userHooksDetail(pluginRoot, homeDir) {
1563
1649
  }
1564
1650
  }
1565
1651
  function writeRollbackPointer(root, entry) {
1566
- const path = (0, import_node_path9.join)(root, "quarantine", "rollback.json");
1652
+ const path = (0, import_node_path10.join)(root, "quarantine", "rollback.json");
1567
1653
  const tmp = `${path}.tmp-${process.pid}`;
1568
1654
  try {
1569
- (0, import_node_fs12.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1570
- (0, import_node_fs12.renameSync)(tmp, path);
1655
+ (0, import_node_fs13.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1656
+ (0, import_node_fs13.renameSync)(tmp, path);
1571
1657
  } catch {
1572
1658
  discard(tmp);
1573
1659
  }
1574
1660
  }
1575
1661
  function mtimeMs(path) {
1576
1662
  try {
1577
- return (0, import_node_fs12.statSync)(path).mtimeMs;
1663
+ return (0, import_node_fs13.statSync)(path).mtimeMs;
1578
1664
  } catch {
1579
1665
  return 0;
1580
1666
  }
1581
1667
  }
1582
1668
  function pruneQuarantine(root, keep) {
1583
- const dir = (0, import_node_path9.join)(root, "quarantine");
1669
+ const dir = (0, import_node_path10.join)(root, "quarantine");
1584
1670
  let generations;
1585
1671
  try {
1586
- generations = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path9.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1672
+ generations = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path10.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1587
1673
  } catch {
1588
1674
  return 0;
1589
1675
  }
1590
1676
  let stranded = 0;
1591
1677
  try {
1592
- for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
1678
+ for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
1593
1679
  if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1594
- const path = (0, import_node_path9.join)(dir, entry.name);
1680
+ const path = (0, import_node_path10.join)(dir, entry.name);
1595
1681
  if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
1596
1682
  discard(path);
1597
1683
  stranded += 1;
@@ -1609,16 +1695,16 @@ function pruneQuarantine(root, keep) {
1609
1695
  }
1610
1696
  function restorePrevious(live, quarantined, broken) {
1611
1697
  try {
1612
- (0, import_node_fs12.renameSync)(live, broken);
1698
+ (0, import_node_fs13.renameSync)(live, broken);
1613
1699
  } catch {
1614
1700
  return "kept-broken";
1615
1701
  }
1616
1702
  try {
1617
- (0, import_node_fs12.renameSync)(quarantined, live);
1703
+ (0, import_node_fs13.renameSync)(quarantined, live);
1618
1704
  return "restored";
1619
1705
  } catch {
1620
1706
  try {
1621
- (0, import_node_fs12.renameSync)(broken, live);
1707
+ (0, import_node_fs13.renameSync)(broken, live);
1622
1708
  return "kept-broken";
1623
1709
  } catch {
1624
1710
  return "lost";
@@ -1629,16 +1715,17 @@ function cursorArm(options) {
1629
1715
  const env = options.env ?? process.env;
1630
1716
  const { target, dryRun, paths } = options;
1631
1717
  const root = cursorPluginsRoot();
1632
- const live = (0, import_node_path9.join)(root, "local", "mmi");
1633
- const result = (from, to, verdict, detail) => ({
1718
+ const live = (0, import_node_path10.join)(root, "local", "mmi");
1719
+ const result = (from, to, verdict, detail, launchStaged = false) => ({
1634
1720
  surface: "cursor",
1635
1721
  from,
1636
1722
  to,
1637
1723
  verdict,
1638
1724
  detail,
1725
+ launchStaged: launchStaged || void 0,
1639
1726
  // C11: the constraint of the generation LEFT at `live` — read at result time, so a failed
1640
1727
  // switch reports the restored generation's declaration, not the rejected one's (#4973).
1641
- compat: readManifestCompat((0, import_node_path9.join)(live, MANIFEST))
1728
+ compat: readManifestCompat((0, import_node_path10.join)(live, MANIFEST))
1642
1729
  });
1643
1730
  const current = manifest(live);
1644
1731
  const installed = current?.version ?? null;
@@ -1652,7 +1739,7 @@ function cursorArm(options) {
1652
1739
  if (installed === target && treeHealthy2(live)) {
1653
1740
  return result(installed, target, "ok", `already at target ${target}${userHooksDetail(live, (0, import_node_os5.homedir)())}`);
1654
1741
  }
1655
- if ((0, import_node_fs12.existsSync)(live) && !mmiOwned(live)) {
1742
+ if ((0, import_node_fs13.existsSync)(live) && !mmiOwned(live)) {
1656
1743
  return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
1657
1744
  }
1658
1745
  if (dryRun) {
@@ -1666,43 +1753,63 @@ function cursorArm(options) {
1666
1753
  if (stagedMarker !== target || !treeHealthy2(staged.packageRoot)) {
1667
1754
  return result(installed, target, "defer", `staged tree marker is ${stagedMarker ?? "unreadable"} and the tree is ${treeHealthy2(staged.packageRoot) ? "complete" : "incomplete"}, expected ${target} and complete`);
1668
1755
  }
1756
+ const launchTargetPath = (0, import_node_path10.join)(root, "staging", "launch-target.json");
1757
+ const priorLaunch = readLaunchTarget(launchTargetPath);
1758
+ let incoming = priorLaunch?.target === target && priorLaunch.incoming.startsWith((0, import_node_path10.join)(root, "staging")) && manifest(priorLaunch.incoming)?.version === target && treeHealthy2(priorLaunch.incoming) ? priorLaunch.incoming : (0, import_node_path10.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1759
+ const reuseIncoming = incoming === priorLaunch?.incoming;
1760
+ if (!reuseIncoming || !(0, import_node_fs13.existsSync)(incoming)) {
1761
+ incoming = (0, import_node_path10.join)(root, "staging", `mmi-${safeSegment(target)}-${Date.now()}`);
1762
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(root, dir), { recursive: true });
1763
+ try {
1764
+ discard(incoming);
1765
+ (0, import_node_fs13.cpSync)(staged.packageRoot, incoming, { recursive: true });
1766
+ } catch (error) {
1767
+ discard(incoming);
1768
+ return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
1769
+ }
1770
+ if (manifest(incoming)?.version !== target || !treeHealthy2(incoming)) {
1771
+ discard(incoming);
1772
+ return result(installed, target, "defer", "the copy on the Cursor volume did not reproduce the verified tree");
1773
+ }
1774
+ }
1669
1775
  const busy = cursorHostEvidence(env);
1670
1776
  if (busy) {
1671
- const pending = dryRun ? "" : ` \u2014 ${noteBusyDefer(paths, env, { surface: "cursor", target, reason: busy, images: ["Cursor"] })}`;
1672
- return result(installed, target, "defer", `Cursor is running (${busy}) \u2014 the plugin tree is only swapped while the host is absent${pending}`);
1777
+ const pending = noteLaunchStaged(paths, env, {
1778
+ surface: "cursor",
1779
+ target,
1780
+ reason: busy,
1781
+ launchTargetPath,
1782
+ incoming,
1783
+ images: ["Cursor"]
1784
+ });
1785
+ return result(installed, target, "ok", `staged ${target} for next launch while Cursor is running (${busy}) \u2014 ${pending}`, true);
1673
1786
  }
1674
1787
  const stamp = Date.now();
1675
- const incoming = (0, import_node_path9.join)(root, "staging", `mmi-${safeSegment(target)}-${stamp}`);
1676
- const quarantined = (0, import_node_path9.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1677
- for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs12.mkdirSync)((0, import_node_path9.join)(root, dir), { recursive: true });
1678
- try {
1679
- discard(incoming);
1680
- (0, import_node_fs12.cpSync)(staged.packageRoot, incoming, { recursive: true });
1681
- } catch (error) {
1682
- discard(incoming);
1683
- return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
1684
- }
1685
- if (manifest(incoming)?.version !== target || !treeHealthy2(incoming)) {
1686
- discard(incoming);
1687
- return result(installed, target, "defer", "the copy on the Cursor volume did not reproduce the verified tree");
1688
- }
1788
+ const quarantined = (0, import_node_path10.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1789
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs13.mkdirSync)((0, import_node_path10.join)(root, dir), { recursive: true });
1689
1790
  const started = cursorHostEvidence(env);
1690
1791
  if (started) {
1691
- discard(incoming);
1692
- const pending = noteBusyDefer(paths, env, { surface: "cursor", target, reason: started, images: ["Cursor"] });
1693
- return result(installed, target, "defer", `Cursor started during staging (${started}) \u2014 the plugin tree is only swapped while the host is absent \u2014 ${pending}`);
1792
+ const pending = noteLaunchStaged(paths, env, {
1793
+ surface: "cursor",
1794
+ target,
1795
+ reason: started,
1796
+ launchTargetPath,
1797
+ incoming,
1798
+ images: ["Cursor"]
1799
+ });
1800
+ return result(installed, target, "ok", `staged ${target} for next launch \u2014 Cursor started during staging (${started}) \u2014 ${pending}`, true);
1694
1801
  }
1695
1802
  let displaced = false;
1696
1803
  try {
1697
- if ((0, import_node_fs12.existsSync)(live)) {
1698
- (0, import_node_fs12.renameSync)(live, quarantined);
1804
+ if ((0, import_node_fs13.existsSync)(live)) {
1805
+ (0, import_node_fs13.renameSync)(live, quarantined);
1699
1806
  displaced = true;
1700
1807
  }
1701
- (0, import_node_fs12.renameSync)(incoming, live);
1808
+ (0, import_node_fs13.renameSync)(incoming, live);
1702
1809
  } catch (error) {
1703
- if (displaced && !(0, import_node_fs12.existsSync)(live)) {
1810
+ if (displaced && !(0, import_node_fs13.existsSync)(live)) {
1704
1811
  try {
1705
- (0, import_node_fs12.renameSync)(quarantined, live);
1812
+ (0, import_node_fs13.renameSync)(quarantined, live);
1706
1813
  } catch {
1707
1814
  }
1708
1815
  }
@@ -1713,21 +1820,25 @@ function cursorArm(options) {
1713
1820
  }
1714
1821
  const evidence = manifest(live)?.version;
1715
1822
  if (evidence !== target || !treeHealthy2(live)) {
1716
- const broken = `${(0, import_node_path9.join)(root, "quarantine", `mmi-${safeSegment(evidence ?? "broken")}-${stamp}`)}-failed`;
1823
+ const broken = `${(0, import_node_path10.join)(root, "quarantine", `mmi-${safeSegment(evidence ?? "broken")}-${stamp}`)}-failed`;
1717
1824
  const outcome = displaced ? restorePrevious(live, quarantined, broken) : "none";
1718
1825
  const detail = outcome === "restored" ? `restored the previous generation ${installed ?? "unknown"}, unverified tree kept at ${broken}` : outcome === "lost" ? `restore failed and ${live} is missing \u2014 the previous generation is at ${quarantined}` : displaced ? `restore failed, the unverified tree is still live and the previous generation is at ${quarantined}` : "nothing was displaced \u2014 there is no previous generation to restore";
1719
1826
  writeRollbackPointer(root, outcome === "restored" ? { installed: installed ?? "unknown", previous: null, generation: null } : { installed: evidence ?? "unknown", previous: installed, generation: displaced ? quarantined : null });
1720
1827
  return result(installed, outcome === "restored" ? installed ?? "unknown" : evidence ?? "unknown", "fail", `post-switch marker reads ${evidence ?? "missing"}, expected ${target}; ${detail}`);
1721
1828
  }
1722
1829
  writeRollbackPointer(root, { installed: target, previous: installed, generation: displaced ? quarantined : null });
1830
+ clearLaunchTarget(launchTargetPath);
1723
1831
  const pruned = pruneQuarantine(root, displaced ? quarantined : null);
1724
1832
  const previous = displaced ? `previous generation quarantined at ${quarantined}` : "no previous generation to quarantine";
1725
1833
  return result(installed, target, "ok", `switched ${installed ?? "absent"} -> ${target}; ${previous}${pruned ? `; pruned ${pruned} older generation${pruned === 1 ? "" : "s"}` : ""}${userHooksDetail(live, (0, import_node_os5.homedir)())}`);
1726
1834
  }
1835
+ function probeCursorLaunchStaging() {
1836
+ return probeLaunchTargetVersion((0, import_node_path10.join)(cursorPluginsRoot(), "staging", "launch-target.json"), (root) => manifest(root)?.version ?? null);
1837
+ }
1727
1838
 
1728
1839
  // src/arm-jervcode.ts
1729
- var import_node_fs13 = require("node:fs");
1730
- var import_node_path10 = require("node:path");
1840
+ var import_node_fs14 = require("node:fs");
1841
+ var import_node_path11 = require("node:path");
1731
1842
  var PI_PACKAGE = "@mutmutco/pi-plugin";
1732
1843
  function hostEnv(env) {
1733
1844
  const dir = jervcodeAgentDir(env);
@@ -1738,7 +1849,7 @@ function hostCommands(_env) {
1738
1849
  }
1739
1850
  function readPackageSpecs(env) {
1740
1851
  try {
1741
- const settings = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1852
+ const settings = JSON.parse((0, import_node_fs14.readFileSync)((0, import_node_path11.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1742
1853
  return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1743
1854
  } catch {
1744
1855
  return null;
@@ -1746,14 +1857,14 @@ function readPackageSpecs(env) {
1746
1857
  }
1747
1858
  function installedVersion(env) {
1748
1859
  try {
1749
- const manifest2 = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1860
+ const manifest2 = JSON.parse((0, import_node_fs14.readFileSync)((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1750
1861
  return typeof manifest2.version === "string" ? manifest2.version : null;
1751
1862
  } catch {
1752
1863
  return null;
1753
1864
  }
1754
1865
  }
1755
1866
  function probeJervcodeInstallation(env = process.env) {
1756
- const location = (0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1867
+ const location = (0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1757
1868
  const version = installedVersion(env);
1758
1869
  return { version, location, detail: version ? "materialized pi package manifest" : "materialized pi package manifest was unreadable" };
1759
1870
  }
@@ -1775,32 +1886,32 @@ function jervcodeArm(options) {
1775
1886
  detail,
1776
1887
  // C11: the constraint of the package pi LEFT materialised — read at result time, so defer/fail
1777
1888
  // carry the previous payload's declaration (#4973 reconsult ruling).
1778
- compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"))
1889
+ compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"))
1779
1890
  });
1780
1891
  const before = readPackageSpecs(env);
1781
- if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path10.join)(jervcodeAgentDir(env), "settings.json")}`);
1892
+ if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path11.join)(jervcodeAgentDir(env), "settings.json")}`);
1782
1893
  const from = installedVersion(env);
1783
1894
  const legacy = before.filter(isLegacyMmiPiPath);
1784
1895
  const highWater = journalHighWater(options.paths.journalPath, "jervcode");
1785
1896
  const floor = from && compareSemver(from, options.target) > 0 ? `installed ${from} is above candidate ${options.target} (monotonic)` : highWater && compareSemver(highWater, options.target) > 0 ? `journal high-water ${highWater} is above candidate ${options.target} (monotonic)` : null;
1786
1897
  if (options.dryRun) {
1787
1898
  const action = floor ? `hold ${from ?? "the install"} and skip the install (${floor})` : before.includes(exact) && from === options.target ? "keep exact pin" : `install ${exact}`;
1788
- return { surface: "jervcode", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacy.length ? ` and retire ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : ""}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1899
+ return { surface: "jervcode", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacy.length ? ` and retire ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : ""}`, compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1789
1900
  }
1790
1901
  const hosted = hostEnv(env);
1791
1902
  const commands = hostCommands(env);
1792
1903
  const command = commands.find((bin) => runner(bin, ["--version"], hosted).status === 0);
1793
- if (!command) return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: "neither `jervcode` nor `pi` answered --version \u2014 the host CLI is not on PATH", compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1904
+ if (!command) return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: "neither `jervcode` nor `pi` answered --version \u2014 the host CLI is not on PATH", compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1794
1905
  if (!floor && (!before.includes(exact) || from !== options.target)) {
1795
1906
  const install = runner(command, ["install", exact], hosted);
1796
1907
  if (install.status !== 0) {
1797
- return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} install ${exact} failed: ${failure2(install)}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1908
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} install ${exact} failed: ${failure2(install)}`, compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1798
1909
  }
1799
1910
  }
1800
1911
  for (const stale of legacy) {
1801
1912
  const removed = runner(command, ["remove", stale], hosted);
1802
1913
  if (removed.status !== 0) {
1803
- return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} remove of the legacy registration failed: ${failure2(removed)}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1914
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} remove of the legacy registration failed: ${failure2(removed)}`, compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1804
1915
  }
1805
1916
  }
1806
1917
  const after = readPackageSpecs(env);
@@ -1808,7 +1919,7 @@ function jervcodeArm(options) {
1808
1919
  const legacyAfter = after?.filter(isLegacyMmiPiPath) ?? [];
1809
1920
  const retired = legacy.length ? `; retired ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : "";
1810
1921
  if (floor && !legacyAfter.length) {
1811
- return { surface: "jervcode", from, to: converged ?? highWater ?? options.target, verdict: "skip", detail: `${floor}${retired}`, compat: readManifestCompat((0, import_node_path10.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1922
+ return { surface: "jervcode", from, to: converged ?? highWater ?? options.target, verdict: "skip", detail: `${floor}${retired}`, compat: readManifestCompat((0, import_node_path11.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1812
1923
  }
1813
1924
  if (!after?.includes(exact) || converged !== options.target || legacyAfter.length) {
1814
1925
  return {
@@ -1829,15 +1940,15 @@ function jervcodeArm(options) {
1829
1940
  }
1830
1941
 
1831
1942
  // src/arm-hermes.ts
1832
- var import_node_fs14 = require("node:fs");
1943
+ var import_node_fs15 = require("node:fs");
1833
1944
  var import_node_child_process9 = require("node:child_process");
1834
- var import_node_path11 = require("node:path");
1945
+ var import_node_path12 = require("node:path");
1835
1946
  var PACKAGE2 = "@mutmutco/hermes-plugin";
1836
1947
  var MANIFEST2 = "plugin.yaml";
1837
1948
  var TREE_MARKERS2 = [MANIFEST2, "__init__.py", "skills"];
1838
1949
  function yamlVersion(root) {
1839
1950
  try {
1840
- const text = (0, import_node_fs14.readFileSync)((0, import_node_path11.join)(root, MANIFEST2), "utf8");
1951
+ const text = (0, import_node_fs15.readFileSync)((0, import_node_path12.join)(root, MANIFEST2), "utf8");
1841
1952
  try {
1842
1953
  const version = JSON.parse(text).version;
1843
1954
  if (typeof version === "string" && version.trim()) return version.trim();
@@ -1850,9 +1961,9 @@ function yamlVersion(root) {
1850
1961
  }
1851
1962
  }
1852
1963
  function treeHealthy3(root) {
1853
- return TREE_MARKERS2.slice(0, 2).every((file) => (0, import_node_fs14.existsSync)((0, import_node_path11.join)(root, file))) && (() => {
1964
+ return TREE_MARKERS2.slice(0, 2).every((file) => (0, import_node_fs15.existsSync)((0, import_node_path12.join)(root, file))) && (() => {
1854
1965
  try {
1855
- return (0, import_node_fs14.statSync)((0, import_node_path11.join)(root, "skills")).isDirectory();
1966
+ return (0, import_node_fs15.statSync)((0, import_node_path12.join)(root, "skills")).isDirectory();
1856
1967
  } catch {
1857
1968
  return false;
1858
1969
  }
@@ -1860,13 +1971,13 @@ function treeHealthy3(root) {
1860
1971
  }
1861
1972
  function packageIdentity(root) {
1862
1973
  try {
1863
- return JSON.parse((0, import_node_fs14.readFileSync)((0, import_node_path11.join)(root, "package.json"), "utf8")).name === PACKAGE2;
1974
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path12.join)(root, "package.json"), "utf8")).name === PACKAGE2;
1864
1975
  } catch {
1865
1976
  return false;
1866
1977
  }
1867
1978
  }
1868
1979
  function probeHermesInstallation(env = process.env) {
1869
- const location = (0, import_node_path11.join)(hermesHome(env), "plugins", "mmi");
1980
+ const location = (0, import_node_path12.join)(hermesHome(env), "plugins", "mmi");
1870
1981
  const version = yamlVersion(location);
1871
1982
  return { version, location, detail: version ? "installed Hermes plugin manifest" : "installed Hermes plugin manifest was unreadable" };
1872
1983
  }
@@ -1875,7 +1986,7 @@ function mmiOwned2(root) {
1875
1986
  }
1876
1987
  function discard2(path) {
1877
1988
  try {
1878
- (0, import_node_fs14.rmSync)(path, { recursive: true, force: true });
1989
+ (0, import_node_fs15.rmSync)(path, { recursive: true, force: true });
1879
1990
  } catch {
1880
1991
  }
1881
1992
  }
@@ -1883,13 +1994,15 @@ function message2(error) {
1883
1994
  return String(error?.message ?? error).replace(/\s+/g, " ").slice(0, 200);
1884
1995
  }
1885
1996
  var HERMES_PROCESS_SEGMENT = /^hermes(-cli|-code)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/i;
1997
+ var HUB_UPDATE_COMMAND = /(?:\bmmi-hub(?:\.(?:cmd|exe|bat|ps1))?\b|@mutmutco[/\\]hub[/\\]dist[/\\]index\.(?:cjs|mjs|js))[\s'"]+update\b/i;
1886
1998
  function hermesProcessToken(line, home) {
1887
1999
  const normalized = line.replace(/["']/g, " ");
1888
- if (home && normalized.toLowerCase().includes(home.toLowerCase())) return home;
1889
- for (const token of normalized.split(/\s+/)) {
1890
- if (token && token.split(/[/\\]/).some((segment) => HERMES_PROCESS_SEGMENT.test(segment))) return token;
1891
- }
1892
- return null;
2000
+ const tokens = normalized.split(/\s+/);
2001
+ const hermesToken = (candidates) => candidates.find((token) => token && token.split(/[/\\]/).some((segment) => HERMES_PROCESS_SEGMENT.test(segment))) ?? null;
2002
+ const direct = hermesToken(tokens.slice(0, 2));
2003
+ if (direct) return direct;
2004
+ if (HUB_UPDATE_COMMAND.test(line)) return null;
2005
+ return hermesToken(tokens) ?? (home && normalized.toLowerCase().includes(home.toLowerCase()) ? home : null);
1893
2006
  }
1894
2007
  function realProcessListing() {
1895
2008
  const result = process.platform === "win32" ? (0, import_node_child_process9.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process9.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
@@ -1905,19 +2018,19 @@ function hermesHostEvidence(env, home, processListProvider) {
1905
2018
  return null;
1906
2019
  }
1907
2020
  function materializeHermesSkills(home) {
1908
- const pluginSkills = (0, import_node_path11.join)(home, "plugins", "mmi", "skills");
1909
- const skillsRoot = (0, import_node_path11.join)(home, "skills");
1910
- const targetRoot = (0, import_node_path11.join)(skillsRoot, "mmi");
2021
+ const pluginSkills = (0, import_node_path12.join)(home, "plugins", "mmi", "skills");
2022
+ const skillsRoot = (0, import_node_path12.join)(home, "skills");
2023
+ const targetRoot = (0, import_node_path12.join)(skillsRoot, "mmi");
1911
2024
  try {
1912
- if (!(0, import_node_fs14.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
1913
- const names = (0, import_node_fs14.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
2025
+ if (!(0, import_node_fs15.existsSync)(pluginSkills)) return { ok: false, detail: `plugin skills tree missing at ${pluginSkills}` };
2026
+ const names = (0, import_node_fs15.readdirSync)(pluginSkills, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
1914
2027
  if (!names.length) return { ok: false, detail: `plugin skills tree is empty at ${pluginSkills}` };
1915
- (0, import_node_fs14.mkdirSync)(skillsRoot, { recursive: true });
1916
- const incoming = (0, import_node_path11.join)(skillsRoot, ".mmi-incoming");
2028
+ (0, import_node_fs15.mkdirSync)(skillsRoot, { recursive: true });
2029
+ const incoming = (0, import_node_path12.join)(skillsRoot, ".mmi-incoming");
1917
2030
  discard2(incoming);
1918
- (0, import_node_fs14.cpSync)(pluginSkills, incoming, { recursive: true });
2031
+ (0, import_node_fs15.cpSync)(pluginSkills, incoming, { recursive: true });
1919
2032
  discard2(targetRoot);
1920
- (0, import_node_fs14.renameSync)(incoming, targetRoot);
2033
+ (0, import_node_fs15.renameSync)(incoming, targetRoot);
1921
2034
  return { ok: true, detail: `provisioned ${names.length} skills to ${targetRoot}` };
1922
2035
  } catch (error) {
1923
2036
  return { ok: false, detail: message2(error) };
@@ -1926,18 +2039,19 @@ function materializeHermesSkills(home) {
1926
2039
  function hermesArm(options) {
1927
2040
  const env = options.env ?? process.env;
1928
2041
  const home = hermesHome(env);
1929
- const parent = (0, import_node_path11.join)(home, "plugins");
1930
- const live = (0, import_node_path11.join)(parent, "mmi");
2042
+ const parent = (0, import_node_path12.join)(home, "plugins");
2043
+ const live = (0, import_node_path12.join)(parent, "mmi");
1931
2044
  const installed = yamlVersion(live);
1932
- const result = (from, to, verdict, detail) => ({
2045
+ const result = (from, to, verdict, detail, launchStaged = false) => ({
1933
2046
  surface: "hermes",
1934
2047
  from,
1935
2048
  to,
1936
2049
  verdict,
1937
- detail
2050
+ detail,
2051
+ launchStaged: launchStaged || void 0
1938
2052
  });
1939
- if (!(0, import_node_fs14.existsSync)(home)) return result(null, options.target, "skip", `absent (${home}) \u2014 skipped without writes`);
1940
- if ((0, import_node_fs14.existsSync)(live) && !mmiOwned2(live)) {
2053
+ if (!(0, import_node_fs15.existsSync)(home)) return result(null, options.target, "skip", `absent (${home}) \u2014 skipped without writes`);
2054
+ if ((0, import_node_fs15.existsSync)(live) && !mmiOwned2(live)) {
1941
2055
  return result(installed, options.target, "defer", `${live} carries no ${PACKAGE2} package identity \u2014 refusing to displace an unmanaged Hermes mmi plugin`);
1942
2056
  }
1943
2057
  if (installed && compareSemver(installed, options.target) > 0) return result(installed, installed, "skip", `installed ${installed} is above candidate ${options.target} (monotonic)`);
@@ -1948,41 +2062,53 @@ function hermesArm(options) {
1948
2062
  if (!skills2.ok) return result(installed, options.target, "fail", `plugin at target ${options.target}, but skills provisioning failed: ${skills2.detail}`);
1949
2063
  return result(installed, options.target, "ok", `already at target ${options.target}; ${skills2.detail}`);
1950
2064
  }
1951
- if (options.dryRun) return result(installed, options.target, "ok", `dry-run: would stage ${PACKAGE2}@${options.target}, atomically switch ${installed ?? "absent"} -> ${options.target}, then provision skills into ${(0, import_node_path11.join)(home, "skills", "mmi")}`);
2065
+ if (options.dryRun) return result(installed, options.target, "ok", `dry-run: would stage ${PACKAGE2}@${options.target}, atomically switch ${installed ?? "absent"} -> ${options.target}, then provision skills into ${(0, import_node_path12.join)(home, "skills", "mmi")}`);
1952
2066
  const staged = stagePackage({ surface: "hermes", packageName: PACKAGE2, target: options.target, witness: MANIFEST2, paths: options.paths, env });
1953
2067
  if ("defer" in staged) return result(installed, options.target, "defer", staged.defer);
1954
2068
  if (yamlVersion(staged.packageRoot) !== options.target || !treeHealthy3(staged.packageRoot)) {
1955
2069
  return result(installed, options.target, "defer", `staged Hermes manifest is ${yamlVersion(staged.packageRoot) ?? "unreadable"} or its tree is incomplete (expected ${options.target})`);
1956
2070
  }
1957
- const scratch = (0, import_node_path11.join)(home, ".mmi-updater");
1958
- const incomingRoot = (0, import_node_path11.join)(scratch, "incoming");
1959
- const incoming = (0, import_node_path11.join)(incomingRoot, options.target);
1960
- const quarantine = (0, import_node_path11.join)(scratch, "quarantine", `${installed ?? "unknown"}-${Date.now()}`);
1961
- try {
1962
- (0, import_node_fs14.mkdirSync)(parent, { recursive: true });
1963
- (0, import_node_fs14.mkdirSync)((0, import_node_path11.join)(scratch, "quarantine"), { recursive: true });
1964
- discard2(incomingRoot);
1965
- (0, import_node_fs14.cpSync)(staged.packageRoot, incoming, { recursive: true });
1966
- } catch (error) {
1967
- return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
1968
- }
1969
- if (yamlVersion(incoming) !== options.target || !treeHealthy3(incoming)) {
1970
- discard2(incomingRoot);
1971
- return result(installed, options.target, "defer", "incoming Hermes generation did not reproduce the verified payload");
2071
+ const scratch = (0, import_node_path12.join)(home, ".mmi-updater");
2072
+ const launchTargetPath = (0, import_node_path12.join)(scratch, "launch-target.json");
2073
+ const incomingRoot = (0, import_node_path12.join)(scratch, "incoming");
2074
+ const incoming = (0, import_node_path12.join)(incomingRoot, options.target);
2075
+ const quarantine = (0, import_node_path12.join)(scratch, "quarantine", `${installed ?? "unknown"}-${Date.now()}`);
2076
+ const priorLaunch = readLaunchTarget(launchTargetPath);
2077
+ const reuseIncoming = priorLaunch?.target === options.target && priorLaunch.incoming === incoming && yamlVersion(incoming) === options.target && treeHealthy3(incoming);
2078
+ if (!reuseIncoming) {
2079
+ try {
2080
+ (0, import_node_fs15.mkdirSync)(parent, { recursive: true });
2081
+ (0, import_node_fs15.mkdirSync)((0, import_node_path12.join)(scratch, "quarantine"), { recursive: true });
2082
+ discard2(incomingRoot);
2083
+ (0, import_node_fs15.cpSync)(staged.packageRoot, incoming, { recursive: true });
2084
+ } catch (error) {
2085
+ return result(installed, options.target, "defer", `cannot place incoming Hermes generation: ${message2(error)}`);
2086
+ }
2087
+ if (yamlVersion(incoming) !== options.target || !treeHealthy3(incoming)) {
2088
+ discard2(incomingRoot);
2089
+ return result(installed, options.target, "defer", "incoming Hermes generation did not reproduce the verified payload");
2090
+ }
1972
2091
  }
1973
2092
  const busy = hermesHostEvidence(env, home, options.processListProvider);
1974
2093
  if (busy) {
1975
- discard2(incomingRoot);
1976
- const pending = noteBusyDefer(options.paths, env, { surface: "hermes", target: options.target, reason: busy, images: ["hermes"], dirs: [home] });
1977
- return result(installed, options.target, "defer", `Hermes may be running \u2014 the plugin tree is only swapped while the host is absent: ${busy} \u2014 ${pending}`);
2094
+ const pending = noteLaunchStaged(options.paths, env, {
2095
+ surface: "hermes",
2096
+ target: options.target,
2097
+ reason: busy,
2098
+ launchTargetPath,
2099
+ incoming,
2100
+ images: ["hermes"],
2101
+ dirs: [home]
2102
+ });
2103
+ return result(installed, options.target, "ok", `staged ${options.target} for next launch while Hermes is running (${busy}) \u2014 ${pending}`, true);
1978
2104
  }
1979
- const displaced = (0, import_node_fs14.existsSync)(live);
2105
+ const displaced = (0, import_node_fs15.existsSync)(live);
1980
2106
  try {
1981
- if (displaced) (0, import_node_fs14.renameSync)(live, quarantine);
1982
- (0, import_node_fs14.renameSync)(incoming, live);
2107
+ if (displaced) (0, import_node_fs15.renameSync)(live, quarantine);
2108
+ (0, import_node_fs15.renameSync)(incoming, live);
1983
2109
  } catch (error) {
1984
- if (displaced && !(0, import_node_fs14.existsSync)(live)) try {
1985
- (0, import_node_fs14.renameSync)(quarantine, live);
2110
+ if (displaced && !(0, import_node_fs15.existsSync)(live)) try {
2111
+ (0, import_node_fs15.renameSync)(quarantine, live);
1986
2112
  } catch {
1987
2113
  }
1988
2114
  discard2(incomingRoot);
@@ -1991,26 +2117,30 @@ function hermesArm(options) {
1991
2117
  return result(installed, options.target, "defer", `Hermes generation switch failed; previous generation retained: ${reason}${pending}`);
1992
2118
  }
1993
2119
  if (yamlVersion(live) !== options.target || !treeHealthy3(live)) {
1994
- const broken = (0, import_node_path11.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
2120
+ const broken = (0, import_node_path12.join)(scratch, "rejected", `${options.target}-${Date.now()}`);
1995
2121
  try {
1996
- (0, import_node_fs14.mkdirSync)((0, import_node_path11.join)(scratch, "rejected"), { recursive: true });
1997
- (0, import_node_fs14.renameSync)(live, broken);
1998
- if (displaced) (0, import_node_fs14.renameSync)(quarantine, live);
2122
+ (0, import_node_fs15.mkdirSync)((0, import_node_path12.join)(scratch, "rejected"), { recursive: true });
2123
+ (0, import_node_fs15.renameSync)(live, broken);
2124
+ if (displaced) (0, import_node_fs15.renameSync)(quarantine, live);
1999
2125
  } catch (error) {
2000
2126
  return result(installed, yamlVersion(live) ?? "unknown", "fail", `post-switch Hermes verification failed and recovery failed: ${message2(error)}`);
2001
2127
  }
2002
2128
  return result(installed, displaced ? installed ?? "unknown" : "none", "fail", `post-switch Hermes verification failed; rejected generation parked at ${broken}${displaced ? "; previous generation restored" : ""}`);
2003
2129
  }
2004
2130
  discard2(incomingRoot);
2131
+ clearLaunchTarget(launchTargetPath);
2005
2132
  const skills = materializeHermesSkills(home);
2006
2133
  if (!skills.ok) return result(installed, options.target, "fail", `plugin converged to ${options.target}, but skills provisioning failed: ${skills.detail}`);
2007
2134
  return result(installed, options.target, "ok", `converged ${installed ?? "absent"} -> ${options.target} (staged, verified, atomically switched${displaced ? `; previous generation quarantined at ${quarantine}` : ""}); ${skills.detail}`);
2008
2135
  }
2136
+ function probeHermesLaunchStaging(env = process.env) {
2137
+ return probeLaunchTargetVersion((0, import_node_path12.join)(hermesHome(env), ".mmi-updater", "launch-target.json"), yamlVersion);
2138
+ }
2009
2139
 
2010
2140
  // src/reap.ts
2011
2141
  var import_node_child_process10 = require("node:child_process");
2012
- var import_node_fs15 = require("node:fs");
2013
- var import_node_path12 = require("node:path");
2142
+ var import_node_fs16 = require("node:fs");
2143
+ var import_node_path13 = require("node:path");
2014
2144
  var KEEP_GENERATIONS2 = 2;
2015
2145
  var SCRATCH_AGE_MS = 24 * 60 * 6e4;
2016
2146
  var ORPHAN_MIN_AGE_MS = 10 * 6e4;
@@ -2018,7 +2148,7 @@ var SNAPSHOT_TIMEOUT_MS = 2e4;
2018
2148
  var MAX_UNWIND_ROUNDS = 4;
2019
2149
  function listDirs(path) {
2020
2150
  try {
2021
- return (0, import_node_fs15.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2151
+ return (0, import_node_fs16.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2022
2152
  } catch {
2023
2153
  return [];
2024
2154
  }
@@ -2026,7 +2156,7 @@ function listDirs(path) {
2026
2156
  function generationSets(root) {
2027
2157
  const children = listDirs(root);
2028
2158
  if (children.some((name) => parseSemver(name))) return [root];
2029
- return children.map((name) => (0, import_node_path12.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
2159
+ return children.map((name) => (0, import_node_path13.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
2030
2160
  }
2031
2161
  function pruneGenerations(root, category, dryRun) {
2032
2162
  let reaped = 0;
@@ -2039,7 +2169,7 @@ function pruneGenerations(root, category, dryRun) {
2039
2169
  continue;
2040
2170
  }
2041
2171
  try {
2042
- (0, import_node_fs15.rmSync)((0, import_node_path12.join)(set, version), { recursive: true, force: true });
2172
+ (0, import_node_fs16.rmSync)((0, import_node_path13.join)(set, version), { recursive: true, force: true });
2043
2173
  reaped += 1;
2044
2174
  } catch (error) {
2045
2175
  failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
@@ -2053,8 +2183,8 @@ function pruneGenerations(root, category, dryRun) {
2053
2183
  }
2054
2184
  var LEASE_STEAL_SCRATCH = /^lease\.lock\.stale-\d+$/;
2055
2185
  function atomicWriteScratch(path) {
2056
- const literal = (0, import_node_path12.basename)(path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2057
- return { dir: (0, import_node_path12.dirname)(path), pattern: new RegExp(`^${literal}\\.tmp-\\d+$`) };
2186
+ const literal = (0, import_node_path13.basename)(path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2187
+ return { dir: (0, import_node_path13.dirname)(path), pattern: new RegExp(`^${literal}\\.tmp-\\d+$`) };
2058
2188
  }
2059
2189
  function reapScratch(context, dryRun) {
2060
2190
  const category = "scratch";
@@ -2072,7 +2202,7 @@ function reapScratch(context, dryRun) {
2072
2202
  for (const [dir, patterns] of scanned) {
2073
2203
  let entries;
2074
2204
  try {
2075
- entries = (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true });
2205
+ entries = (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true });
2076
2206
  } catch {
2077
2207
  unreadable.push(dir);
2078
2208
  continue;
@@ -2080,10 +2210,10 @@ function reapScratch(context, dryRun) {
2080
2210
  for (const entry of entries) {
2081
2211
  if (!patterns.some((pattern) => pattern.test(entry.name))) continue;
2082
2212
  if (!entry.isFile()) continue;
2083
- const path = (0, import_node_path12.join)(dir, entry.name);
2213
+ const path = (0, import_node_path13.join)(dir, entry.name);
2084
2214
  let ageMs;
2085
2215
  try {
2086
- ageMs = now - (0, import_node_fs15.statSync)(path).mtimeMs;
2216
+ ageMs = now - (0, import_node_fs16.statSync)(path).mtimeMs;
2087
2217
  } catch {
2088
2218
  continue;
2089
2219
  }
@@ -2093,7 +2223,7 @@ function reapScratch(context, dryRun) {
2093
2223
  continue;
2094
2224
  }
2095
2225
  try {
2096
- (0, import_node_fs15.rmSync)(path, { force: true });
2226
+ (0, import_node_fs16.rmSync)(path, { force: true });
2097
2227
  reaped += 1;
2098
2228
  } catch (error) {
2099
2229
  failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
@@ -2333,7 +2463,7 @@ function reconcile(options) {
2333
2463
  if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
2334
2464
  journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
2335
2465
  options.onArm?.(arm);
2336
- if (arm.verdict === "ok" && !options.dryRun) clearPending(paths, arm.surface);
2466
+ if (arm.verdict === "ok" && !options.dryRun && !arm.launchStaged) clearPending(paths, arm.surface);
2337
2467
  if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
2338
2468
  if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
2339
2469
  };
@@ -2438,9 +2568,9 @@ function reconcile(options) {
2438
2568
 
2439
2569
  // src/scheduler.ts
2440
2570
  var import_node_child_process11 = require("node:child_process");
2441
- var import_node_fs16 = require("node:fs");
2571
+ var import_node_fs17 = require("node:fs");
2442
2572
  var import_node_os6 = require("node:os");
2443
- var import_node_path13 = require("node:path");
2573
+ var import_node_path14 = require("node:path");
2444
2574
  var TASK_NAME = "MMI Fleet Updater";
2445
2575
  var LAUNCHER_FILE = "reconcile-hidden.vbs";
2446
2576
  var RUN_BOUND = "PT15M";
@@ -2598,7 +2728,7 @@ WantedBy=timers.target
2598
2728
  }
2599
2729
  function readFileOrNull(path) {
2600
2730
  try {
2601
- return (0, import_node_fs16.readFileSync)(path, "utf8");
2731
+ return (0, import_node_fs17.readFileSync)(path, "utf8");
2602
2732
  } catch {
2603
2733
  return null;
2604
2734
  }
@@ -2647,7 +2777,7 @@ function windowsSchedulerStatus(env) {
2647
2777
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${TASK_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2648
2778
  }
2649
2779
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2650
- const launcherPath = (0, import_node_path13.join)(statePaths(env).root, LAUNCHER_FILE);
2780
+ const launcherPath = (0, import_node_path14.join)(statePaths(env).root, LAUNCHER_FILE);
2651
2781
  const info = taskRunInfo();
2652
2782
  const runInfo = info ?? EMPTY_RUN_INFO;
2653
2783
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
@@ -2672,13 +2802,13 @@ function windowsSchedulerStatus(env) {
2672
2802
  return { ok: true, supported: true, enabled: true, ...runInfo, detail: `"${TASK_NAME}" registered ${CADENCE}; ${points}; ${ran}; next ${runInfo.nextRunTime ?? "unscheduled"}` };
2673
2803
  }
2674
2804
  function darwinSchedulerStatus(env) {
2675
- const plistPath = (0, import_node_path13.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2805
+ const plistPath = (0, import_node_path14.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2676
2806
  const plist = readFileOrNull(plistPath);
2677
2807
  if (!plist) {
2678
2808
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2679
2809
  }
2680
2810
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2681
- const launcherPath = (0, import_node_path13.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2811
+ const launcherPath = (0, import_node_path14.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2682
2812
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2683
2813
  if (!plist.includes(launcherPath)) {
2684
2814
  return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
@@ -2698,15 +2828,15 @@ function darwinSchedulerStatus(env) {
2698
2828
  return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
2699
2829
  }
2700
2830
  function linuxSchedulerStatus(env) {
2701
- const unitDir = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2702
- const timerPath = (0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME);
2831
+ const unitDir = (0, import_node_path14.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2832
+ const timerPath = (0, import_node_path14.join)(unitDir, SYSTEMD_TIMER_NAME);
2703
2833
  const timer = readFileOrNull(timerPath);
2704
- const service = readFileOrNull((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME));
2834
+ const service = readFileOrNull((0, import_node_path14.join)(unitDir, SYSTEMD_SERVICE_NAME));
2705
2835
  if (!timer || !service) {
2706
2836
  return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
2707
2837
  }
2708
2838
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2709
- const launcherPath = (0, import_node_path13.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2839
+ const launcherPath = (0, import_node_path14.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
2710
2840
  const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
2711
2841
  if (!service.includes(launcherPath)) {
2712
2842
  return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
@@ -2745,10 +2875,10 @@ function installWindowsTask(env) {
2745
2875
  const paths = ensureState(env);
2746
2876
  const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
2747
2877
  if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
2748
- const launcherPath = (0, import_node_path13.join)(paths.root, LAUNCHER_FILE);
2749
- (0, import_node_fs16.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2750
- const xmlPath = (0, import_node_path13.join)(paths.root, "task.xml");
2751
- (0, import_node_fs16.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2878
+ const launcherPath = (0, import_node_path14.join)(paths.root, LAUNCHER_FILE);
2879
+ (0, import_node_fs17.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
2880
+ const xmlPath = (0, import_node_path14.join)(paths.root, "task.xml");
2881
+ (0, import_node_fs17.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
2752
2882
  const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
2753
2883
  if (create.status !== 0) {
2754
2884
  const denied = /access is denied/i.test(create.stderr + create.stdout);
@@ -2764,11 +2894,11 @@ function refreshPosixLauncher(env) {
2764
2894
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2765
2895
  if (!hubDist) return null;
2766
2896
  const paths = ensureState(env);
2767
- const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2897
+ const launcherPath = (0, import_node_path14.join)(paths.root, POSIX_LAUNCHER_FILE);
2768
2898
  const desired = launcherSh(process.execPath, hubDist);
2769
2899
  if (readFileOrNull(launcherPath) === desired) return null;
2770
- (0, import_node_fs16.writeFileSync)(launcherPath, desired, "utf8");
2771
- (0, import_node_fs16.chmodSync)(launcherPath, 493);
2900
+ (0, import_node_fs17.writeFileSync)(launcherPath, desired, "utf8");
2901
+ (0, import_node_fs17.chmodSync)(launcherPath, 493);
2772
2902
  return launcherPath;
2773
2903
  }
2774
2904
  function installLaunchd(env) {
@@ -2780,13 +2910,13 @@ function installLaunchd(env) {
2780
2910
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2781
2911
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2782
2912
  const paths = ensureState(env);
2783
- const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2784
- (0, import_node_fs16.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2785
- (0, import_node_fs16.chmodSync)(launcherPath, 493);
2786
- const agentsDir = (0, import_node_path13.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents");
2787
- (0, import_node_fs16.mkdirSync)(agentsDir, { recursive: true });
2788
- const plistPath = (0, import_node_path13.join)(agentsDir, LAUNCHD_PLIST_NAME);
2789
- (0, import_node_fs16.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2913
+ const launcherPath = (0, import_node_path14.join)(paths.root, POSIX_LAUNCHER_FILE);
2914
+ (0, import_node_fs17.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2915
+ (0, import_node_fs17.chmodSync)(launcherPath, 493);
2916
+ const agentsDir = (0, import_node_path14.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents");
2917
+ (0, import_node_fs17.mkdirSync)(agentsDir, { recursive: true });
2918
+ const plistPath = (0, import_node_path14.join)(agentsDir, LAUNCHD_PLIST_NAME);
2919
+ (0, import_node_fs17.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
2790
2920
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2791
2921
  const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
2792
2922
  if (load.status !== 0) {
@@ -2804,13 +2934,13 @@ function installSystemdTimer(env) {
2804
2934
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2805
2935
  if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
2806
2936
  const paths = ensureState(env);
2807
- const launcherPath = (0, import_node_path13.join)(paths.root, POSIX_LAUNCHER_FILE);
2808
- (0, import_node_fs16.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2809
- (0, import_node_fs16.chmodSync)(launcherPath, 493);
2810
- const unitDir = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2811
- (0, import_node_fs16.mkdirSync)(unitDir, { recursive: true });
2812
- (0, import_node_fs16.writeFileSync)((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2813
- (0, import_node_fs16.writeFileSync)((0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2937
+ const launcherPath = (0, import_node_path14.join)(paths.root, POSIX_LAUNCHER_FILE);
2938
+ (0, import_node_fs17.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
2939
+ (0, import_node_fs17.chmodSync)(launcherPath, 493);
2940
+ const unitDir = (0, import_node_path14.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2941
+ (0, import_node_fs17.mkdirSync)(unitDir, { recursive: true });
2942
+ (0, import_node_fs17.writeFileSync)((0, import_node_path14.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
2943
+ (0, import_node_fs17.writeFileSync)((0, import_node_path14.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
2814
2944
  const reload = systemctlUser(["daemon-reload"]);
2815
2945
  if (reload.status !== 0) {
2816
2946
  return { ok: false, detail: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim() || "unknown error \u2014 is a user systemd instance running?"}` };
@@ -2848,28 +2978,28 @@ function uninstallWindowsTask(env) {
2848
2978
  return { ok: true, detail: `automatic updates off \u2014 removed "${TASK_NAME}"; installed tooling was kept` };
2849
2979
  }
2850
2980
  function uninstallLaunchd(env) {
2851
- const plistPath = (0, import_node_path13.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2981
+ const plistPath = (0, import_node_path14.join)((0, import_node_os6.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
2852
2982
  const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
2853
- if (boot.status !== 0 && (0, import_node_fs16.existsSync)(plistPath)) {
2983
+ if (boot.status !== 0 && (0, import_node_fs17.existsSync)(plistPath)) {
2854
2984
  return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
2855
2985
  }
2856
2986
  try {
2857
- (0, import_node_fs16.rmSync)(plistPath, { force: true });
2987
+ (0, import_node_fs17.rmSync)(plistPath, { force: true });
2858
2988
  } catch {
2859
2989
  }
2860
2990
  dropLauncher(env, POSIX_LAUNCHER_FILE);
2861
2991
  return { ok: true, detail: `automatic updates off \u2014 removed "${LAUNCHD_LABEL}"; installed tooling was kept` };
2862
2992
  }
2863
2993
  function uninstallSystemdTimer(env) {
2864
- const unitDir = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2865
- const timerPath = (0, import_node_path13.join)(unitDir, SYSTEMD_TIMER_NAME);
2994
+ const unitDir = (0, import_node_path14.join)((0, import_node_os6.homedir)(), ".config", "systemd", "user");
2995
+ const timerPath = (0, import_node_path14.join)(unitDir, SYSTEMD_TIMER_NAME);
2866
2996
  const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
2867
- if (disable.status !== 0 && (0, import_node_fs16.existsSync)(timerPath)) {
2997
+ if (disable.status !== 0 && (0, import_node_fs17.existsSync)(timerPath)) {
2868
2998
  return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
2869
2999
  }
2870
3000
  try {
2871
- (0, import_node_fs16.rmSync)(timerPath, { force: true });
2872
- (0, import_node_fs16.rmSync)((0, import_node_path13.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
3001
+ (0, import_node_fs17.rmSync)(timerPath, { force: true });
3002
+ (0, import_node_fs17.rmSync)((0, import_node_path14.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
2873
3003
  } catch {
2874
3004
  }
2875
3005
  systemctlUser(["daemon-reload"]);
@@ -2884,16 +3014,16 @@ function uninstallTask(env = process.env) {
2884
3014
  }
2885
3015
  function dropLauncher(env, file) {
2886
3016
  try {
2887
- (0, import_node_fs16.rmSync)((0, import_node_path13.join)(statePaths(env).root, file), { force: true });
3017
+ (0, import_node_fs17.rmSync)((0, import_node_path14.join)(statePaths(env).root, file), { force: true });
2888
3018
  } catch {
2889
3019
  }
2890
3020
  }
2891
3021
 
2892
3022
  // src/status.ts
2893
- var import_node_fs17 = require("node:fs");
3023
+ var import_node_fs18 = require("node:fs");
2894
3024
  function readJournal(path) {
2895
3025
  try {
2896
- const events = (0, import_node_fs17.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
3026
+ const events = (0, import_node_fs18.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
2897
3027
  try {
2898
3028
  return JSON.parse(line);
2899
3029
  } catch {
@@ -2905,7 +3035,8 @@ function readJournal(path) {
2905
3035
  return error?.code === "ENOENT" ? { events: [], error: null } : { events: [], error: `journal unreadable: ${error?.message ?? error}` };
2906
3036
  }
2907
3037
  }
2908
- function versionState(installed, expected) {
3038
+ function versionState(installed, staged, expected) {
3039
+ if (staged && expected && staged === expected && installed !== expected) return "staged";
2909
3040
  if (!installed || !expected) return "unknown";
2910
3041
  const relation = compareSemver(installed, expected);
2911
3042
  return relation === 0 ? "current" : relation > 0 ? "ahead" : "behind";
@@ -2920,28 +3051,36 @@ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1])
2920
3051
  const actual = /* @__PURE__ */ new Map();
2921
3052
  const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
2922
3053
  const globalHubVersion = hubDist ? probeVersion(hubDist) : null;
2923
- actual.set("hub", globalHubVersion ? { version: globalHubVersion, location: hubDist, detail: "absolute global Hub binary" } : { version: hubVersion, location: invokedPath ?? null, detail: "currently executing Hub bundle (global package location unavailable)" });
2924
- actual.set("cli", probeCliInstallation(env));
3054
+ actual.set("hub", globalHubVersion ? { version: globalHubVersion, staged: null, location: hubDist, detail: "absolute global Hub binary" } : { version: hubVersion, staged: null, location: invokedPath ?? null, detail: "currently executing Hub bundle (global package location unavailable)" });
3055
+ const cliProbe = probeCliInstallation(env);
3056
+ actual.set("cli", { version: cliProbe.version, staged: null, location: cliProbe.location, detail: cliProbe.detail });
2925
3057
  for (const surface of enumerateSurfaces(env).filter((probe) => probe.present)) {
2926
3058
  if (surface.id === "claude" || surface.id === "codex" || surface.id === "kilo") {
2927
- actual.set(surface.id, probeNativeHostInstallation(surface.id, env));
3059
+ const probe = probeNativeHostInstallation(surface.id, env);
3060
+ actual.set(surface.id, { version: probe.version, staged: null, location: probe.location, detail: probe.detail });
2928
3061
  } else if (surface.id === "kimi") {
2929
- actual.set(surface.id, probeKimiInstallation(env));
3062
+ const probe = probeKimiInstallation(env);
3063
+ actual.set(surface.id, { version: probe.version, staged: probeKimiLaunchStaging(env), location: probe.location, detail: probe.detail });
2930
3064
  } else if (surface.id === "cursor") {
2931
- actual.set(surface.id, probeCursorInstallation());
3065
+ const probe = probeCursorInstallation();
3066
+ actual.set(surface.id, { version: probe.version, staged: probeCursorLaunchStaging(), location: probe.location, detail: probe.detail });
2932
3067
  } else if (surface.id === "jervcode") {
2933
- actual.set(surface.id, probeJervcodeInstallation(env));
3068
+ const probe = probeJervcodeInstallation(env);
3069
+ actual.set(surface.id, { version: probe.version, staged: null, location: probe.location, detail: probe.detail });
2934
3070
  } else if (surface.id === "hermes") {
2935
- actual.set(surface.id, probeHermesInstallation(env));
3071
+ const probe = probeHermesInstallation(env);
3072
+ actual.set(surface.id, { version: probe.version, staged: probeHermesLaunchStaging(env), location: probe.location, detail: probe.detail });
2936
3073
  }
2937
3074
  }
2938
3075
  const installed = {};
2939
3076
  for (const [surface, probe] of actual) {
3077
+ const staged = probe.staged ?? null;
2940
3078
  installed[surface] = {
2941
3079
  installed: probe.version,
3080
+ staged,
2942
3081
  expected: expectedVersion,
2943
3082
  location: probe.location,
2944
- state: versionState(probe.version, expectedVersion),
3083
+ state: versionState(probe.version, staged, expectedVersion),
2945
3084
  detail: probe.detail
2946
3085
  };
2947
3086
  }
@@ -2978,7 +3117,8 @@ function formatHubStatus(status) {
2978
3117
  " installed:"
2979
3118
  ];
2980
3119
  for (const [surface, row] of Object.entries(status.installed)) {
2981
- lines.push(` ${surface}: ${row.installed ?? "unknown"} (expected ${row.expected ?? "unknown"}; ${row.state})`);
3120
+ const staged = row.staged ? `; staged ${row.staged} for next launch` : "";
3121
+ lines.push(` ${surface}: loaded ${row.installed ?? "unknown"}${staged} (expected ${row.expected ?? "unknown"}; ${row.state})`);
2982
3122
  lines.push(` ${row.location ?? "location unavailable"} \u2014 ${row.detail}`);
2983
3123
  }
2984
3124
  lines.push(` autoupdate: ${status.schedule.enabled ? "on" : "off"} \u2014 ${status.schedule.detail}`);
@@ -3040,8 +3180,8 @@ function failConsequence(row) {
3040
3180
  return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
3041
3181
  }
3042
3182
  function classify(arm) {
3043
- const kind = arm.verdict === "ok" ? arm.from !== null && arm.from === arm.to ? "current" : "updated" : arm.verdict === "skip" ? "keep" : arm.verdict === "defer" ? "retry" : "failed";
3044
- const note = kind === "updated" ? ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
3183
+ const kind = arm.verdict === "ok" ? arm.from !== null && arm.from === arm.to && !arm.launchStaged ? "current" : "updated" : arm.verdict === "skip" ? "keep" : arm.verdict === "defer" ? "retry" : "failed";
3184
+ const note = kind === "updated" ? arm.launchStaged ? "next launch" : ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
3045
3185
  return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail, repeatFailure: arm.repeatFailure === true };
3046
3186
  }
3047
3187
  function statusText(kind, dryRun) {
@@ -3271,7 +3411,7 @@ function startSpinner(enabled) {
3271
3411
  // src/index.ts
3272
3412
  function ownVersion() {
3273
3413
  try {
3274
- return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path14.join)(__dirname, "..", "package.json"), "utf8")).version;
3414
+ return JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path15.join)(__dirname, "..", "package.json"), "utf8")).version;
3275
3415
  } catch {
3276
3416
  return "0.0.0";
3277
3417
  }
@@ -3399,6 +3539,7 @@ if (typeof require !== "undefined" && require.main === module) {
3399
3539
  0 && (module.exports = {
3400
3540
  acquireSharedDocLock,
3401
3541
  buildWatcherCommand,
3542
+ clearLaunchTarget,
3402
3543
  clearPending,
3403
3544
  enumerateSurfaces,
3404
3545
  failConsequence,
@@ -3414,13 +3555,19 @@ if (typeof require !== "undefined" && require.main === module) {
3414
3555
  lockShaped,
3415
3556
  main,
3416
3557
  noteBusyDefer,
3558
+ noteLaunchStaged,
3417
3559
  npmStringField,
3418
3560
  ownVersion,
3419
3561
  parseNpmDistIntegrity,
3420
3562
  pendingPath,
3421
3563
  pidAlive,
3564
+ probeCursorLaunchStaging,
3422
3565
  probeHermesInstallation,
3566
+ probeHermesLaunchStaging,
3567
+ probeKimiLaunchStaging,
3568
+ probeLaunchTargetVersion,
3423
3569
  progressiveRow,
3570
+ readLaunchTarget,
3424
3571
  readPending,
3425
3572
  reconcileCursorUserHooks,
3426
3573
  reconcileKimiInstalledPlugin,
@@ -3440,5 +3587,6 @@ if (typeof require !== "undefined" && require.main === module) {
3440
3587
  watcherLockPath,
3441
3588
  withSharedDocLock,
3442
3589
  wrapWords,
3590
+ writeLaunchTarget,
3443
3591
  writePending
3444
3592
  });