@mu-crew/mu 4.1.1 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ import {
41
41
  steerStallHandler,
42
42
  waitForEvents,
43
43
  waitWithActions
44
- } from "./chunk-B74N2EAS.js";
44
+ } from "./chunk-RJQNDFBU.js";
45
45
  import {
46
46
  FRONTIER_MAX_DEPTH,
47
47
  HumanNeededInvalidError,
@@ -101,7 +101,7 @@ import {
101
101
  taskRecordCut,
102
102
  taskToOutput,
103
103
  workstreamIdOf
104
- } from "./chunk-DEYBQDTO.js";
104
+ } from "./chunk-FVRIKUBR.js";
105
105
  import {
106
106
  AgentAbortNeedsCtlError,
107
107
  AgentAbortTimeoutError,
@@ -127,6 +127,11 @@ import {
127
127
  CTL_OPS,
128
128
  CTL_SOCK_ENV,
129
129
  CURRENT_SCHEMA_VERSION,
130
+ CarrierJobKeptError,
131
+ CarrierJobUnresolvedError,
132
+ CarrierJobUnstoppedError,
133
+ CarrierStopFailedError,
134
+ CarrierStopRefusedError,
130
135
  ClaimerNotRegisteredError,
131
136
  CloseNeedsEvidenceError,
132
137
  CrossWorkstreamEdgeError,
@@ -149,6 +154,7 @@ import {
149
154
  HostChannelBusyError,
150
155
  HostCheckFailedError,
151
156
  HostExistsError,
157
+ HostHandbackError,
152
158
  HostInUseError,
153
159
  HostNotFoundError,
154
160
  HostProfileError,
@@ -260,6 +266,7 @@ import {
260
266
  gitToplevel,
261
267
  groupIdFromPrefix,
262
268
  headSha,
269
+ hostMuleStarter,
263
270
  hostStarter,
264
271
  interruptAgent,
265
272
  isAgentListArgv,
@@ -274,6 +281,7 @@ import {
274
281
  lastClaimActor,
275
282
  latestSeq,
276
283
  listAgents,
284
+ listCarrierJobs,
277
285
  listDelegates,
278
286
  listEmptyWorkstreams,
279
287
  listForgetCandidates,
@@ -350,7 +358,7 @@ import {
350
358
  workstreamRevision,
351
359
  worktreePath,
352
360
  worktreesRoot
353
- } from "./chunk-L7I3BLXP.js";
361
+ } from "./chunk-4ZWBLTZK.js";
354
362
  import {
355
363
  defaultStateDir
356
364
  } from "./chunk-PZ5GOCWH.js";
@@ -3901,7 +3909,10 @@ function classifyError(err) {
3901
3909
  if (err instanceof NameAmbiguousError || // A host profile that exists, is at its channel cap, or still holds channels.
3902
3910
  err instanceof HostExistsError || err instanceof HostChannelBusyError || // A remote worktree close refuses to remove (dirty, uncollected HEAD, moved).
3903
3911
  err instanceof RemoteCloseRefusedError || // A recovery record that is ambiguous, claimed, changed or pinned to another profile.
3904
- err instanceof RemoteRecoveryRefusedError || err instanceof HostInUseError || err instanceof AgentExistsError || err instanceof AgentNameExhaustedError || err instanceof TaskExistsError || err instanceof BackupTargetExistsError || err instanceof TaskAutoRefusedError || err instanceof TaskBriefMissingError || err instanceof DelegateCallConflictError || err instanceof DelegateStateError || err instanceof TaskAlreadyOwnedError || err instanceof TaskHasOpenBlockersError || // Substate refusals (ts_4): a park from the wrong phase, a claim on a
3912
+ err instanceof RemoteRecoveryRefusedError || // A recorded job handle whose stop template changed, or whose profile is gone: nothing ran.
3913
+ err instanceof CarrierStopRefusedError || // A respawn of a name whose older incarnation's job was never stopped.
3914
+ err instanceof CarrierJobUnstoppedError || // A lease close with no handle it can stop, or a reservation it had to keep.
3915
+ err instanceof CarrierJobUnresolvedError || err instanceof CarrierJobKeptError || err instanceof HostInUseError || err instanceof AgentExistsError || err instanceof AgentNameExhaustedError || err instanceof TaskExistsError || err instanceof BackupTargetExistsError || err instanceof TaskAutoRefusedError || err instanceof TaskBriefMissingError || err instanceof DelegateCallConflictError || err instanceof DelegateStateError || err instanceof TaskAlreadyOwnedError || err instanceof TaskHasOpenBlockersError || // Substate refusals (ts_4): a park from the wrong phase, a claim on a
3905
3916
  // parked task, a pair outside TASK_SUBSTATES.
3906
3917
  err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof HumanTaskError || err instanceof TaskInTriageError || err instanceof InvalidSubstateError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof AgentSendUnsupportedError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorktreeDirtyError || err instanceof WorktreeBranchExistsError || err instanceof WorktreeHeldError || err instanceof WorktreePathNotEmptyError || err instanceof WorktreeVcsRequiredError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof SqliteTooOldError || err instanceof SyncSourceTooNewError || err instanceof DbInsideSyncDirError || err instanceof SyncDirForeignError || err instanceof WorkstreamExistsError || err instanceof WorkstreamRevisionMismatchError || err instanceof RevisionMismatchError || err instanceof WorkstreamHasAgentsError || err instanceof WorkstreamPanesLiveError || err instanceof WorkstreamSessionLiveError || // Rebuild refuses to overwrite an existing target or to write onto
3907
3918
  // the source DB: both are name/state collisions, not usage errors.
@@ -3960,9 +3971,13 @@ function classifyError(err) {
3960
3971
  if (err instanceof AgentAbortTimeoutError) {
3961
3972
  return { label: "timeout", exitCode: 5 };
3962
3973
  }
3963
- if (err instanceof RemotePrepareError || err instanceof RemoteCollectError) {
3974
+ if (err instanceof RemotePrepareError || err instanceof RemoteCollectError || // The stop template ran and did not prove the job stopped: the host's answer, substrate.
3975
+ err instanceof CarrierStopFailedError) {
3964
3976
  return { label: "remote host", exitCode: 5 };
3965
3977
  }
3978
+ if (err instanceof HostHandbackError) {
3979
+ return { label: "human needed", exitCode: 5 };
3980
+ }
3966
3981
  if (err instanceof HostCheckFailedError) {
3967
3982
  return { label: "host check", exitCode: 5 };
3968
3983
  }
@@ -4074,7 +4089,8 @@ function emitError(err) {
4074
4089
  error: errClass,
4075
4090
  message: cleanMessage,
4076
4091
  exit: exitCode,
4077
- ...Object.keys(extra).length > 0 ? { extra } : {}
4092
+ ...Object.keys(extra).length > 0 ? { extra } : {},
4093
+ ...err instanceof HostHandbackError ? { data: { handback: true, host: err.host, template: err.field, detail: err.detail } } : {}
4078
4094
  });
4079
4095
  return exitCode;
4080
4096
  }
@@ -4180,12 +4196,20 @@ function findCommandForArgv(root, argv) {
4180
4196
  }
4181
4197
 
4182
4198
  // src/cli/agent-close.ts
4199
+ function carrierJobWorkstreams(db, name) {
4200
+ return listCarrierJobs(db).filter((j) => j.name === name).map((j) => j.workstream);
4201
+ }
4202
+ var hasCarrierJob = (db, ws, name) => carrierJobWorkstreams(db, name).includes(ws);
4183
4203
  async function closeScope(db, name, explicit) {
4184
4204
  if (explicit !== void 0 || injectedWorkstream() !== void 0) {
4185
4205
  return resolveWorkstream(explicit);
4186
4206
  }
4187
4207
  const matches = [
4188
- .../* @__PURE__ */ new Set([...findEntityWorkstreams(db, "agent", name), ...recoveryWorkstreams(db, name)])
4208
+ .../* @__PURE__ */ new Set([
4209
+ ...findEntityWorkstreams(db, "agent", name),
4210
+ ...recoveryWorkstreams(db, name),
4211
+ ...carrierJobWorkstreams(db, name)
4212
+ ])
4189
4213
  ].sort();
4190
4214
  if (matches.length >= 2) throw new NameAmbiguousError(name, matches, "agent");
4191
4215
  const only = matches[0];
@@ -4194,19 +4218,21 @@ async function closeScope(db, name, explicit) {
4194
4218
  async function cmdClose(db, rawName, opts = {}) {
4195
4219
  const name = applyQualifiedRef(rawName, opts);
4196
4220
  const ws = await closeScope(db, name, opts.workstream);
4197
- if (opts.recovery === void 0 && recoveriesFor(db, ws, name).length === 0) {
4221
+ if (opts.recovery === void 0 && recoveriesFor(db, ws, name).length === 0 && !hasCarrierJob(db, ws, name)) {
4198
4222
  assertAgentInWorkstream(db, name, opts.workstream ?? ws);
4199
4223
  }
4200
4224
  const discard = opts.discard === true;
4201
4225
  const keepBranch = opts.keepBranch === true;
4202
4226
  const recoveryId = opts.recovery;
4203
4227
  const force = opts.force === true;
4228
+ const abandonJob = opts.abandonJob === true;
4204
4229
  if (opts.if === void 0) {
4205
4230
  const p = await previewClose(db, name, {
4206
4231
  workstream: ws,
4207
4232
  discard,
4208
4233
  keepBranch,
4209
- ...recoveryId === void 0 ? {} : { recoveryId }
4234
+ ...recoveryId === void 0 ? {} : { recoveryId },
4235
+ ...abandonJob ? { abandonJob: true } : {}
4210
4236
  });
4211
4237
  if (p.kind !== "none" && !p.refusesDirty && p.recovery?.refusal == null) {
4212
4238
  setNextOutcome({
@@ -4221,7 +4247,8 @@ async function cmdClose(db, rawName, opts = {}) {
4221
4247
  ...p.recovery === null ? [] : ["--recovery", String(p.recovery.id)],
4222
4248
  ...discard ? ["--discard"] : [],
4223
4249
  ...keepBranch ? ["--keep-branch"] : [],
4224
- ...force ? ["--force"] : []
4250
+ ...force ? ["--force"] : [],
4251
+ ...abandonJob ? ["--abandon-job"] : []
4225
4252
  ],
4226
4253
  revision: p.revision
4227
4254
  });
@@ -4239,19 +4266,29 @@ async function cmdClose(db, rawName, opts = {}) {
4239
4266
  ...discard ? { discard: true } : {},
4240
4267
  ...keepBranch ? { keepBranch: true } : {},
4241
4268
  ...force ? { force: true } : {},
4269
+ ...abandonJob ? { abandonJob: true } : {},
4242
4270
  ...recoveryId === void 0 ? {} : { recoveryId }
4243
4271
  });
4244
4272
  if (opts.json) {
4245
4273
  emitJson({ agentName: name, ...result });
4246
4274
  return;
4247
4275
  }
4248
- if (!result.killedPane && !result.deletedRow && result.worktree === null) {
4276
+ const jobs = result.carrierJobs ?? [];
4277
+ if (!result.killedPane && !result.deletedRow && result.worktree === null && jobs.length === 0) {
4249
4278
  console.log(pc.dim(`no agent named ${name} (already closed?)`));
4250
4279
  return;
4251
4280
  }
4252
4281
  console.log(
4253
- result.recovery === void 0 ? `Closed ${pc.bold(name)}` : `Closed the remote worktree ${pc.bold(name)} left (recovery ${result.recovery.id})`
4282
+ result.recovery !== void 0 ? `Closed the remote worktree ${pc.bold(name)} left (recovery ${result.recovery.id})` : !result.killedPane && result.worktree === null ? `Closed the lease job ${pc.bold(name)} left` : `Closed ${pc.bold(name)}`
4254
4283
  );
4284
+ for (const j of jobs) {
4285
+ const what = j.job === null ? "lease reservation" : `job ${j.job}`;
4286
+ console.log(
4287
+ pc.dim(
4288
+ j.action === "stopped" ? ` ${what} on ${j.host} stopped` : j.action === "abandon" ? ` ${what} on ${j.host} abandoned without a stop: it may still run there` : ` ${what} on ${j.host} released (its carrier was never launched)`
4289
+ )
4290
+ );
4291
+ }
4255
4292
  if (result.worktree !== null) {
4256
4293
  const w = result.worktree;
4257
4294
  console.log(
@@ -4262,11 +4299,28 @@ async function cmdClose(db, rawName, opts = {}) {
4262
4299
  for (const warn of w.warnings ?? []) console.error(pc.yellow(`warning: ${warn}`));
4263
4300
  }
4264
4301
  }
4302
+ function printCarrierJob(p) {
4303
+ const j = p.carrierJob;
4304
+ if (j === null) return;
4305
+ const what = j.job !== null ? `job ${j.job}` : `a lease reservation with no job handle (launch ${j.launch}, pane ${j.paneId ?? "none"}${j.reason === null ? "" : `, ${j.reason}`})`;
4306
+ console.log(
4307
+ p.abandonJob ? pc.yellow(
4308
+ ` ${what} on ${j.host}: --abandon-job drops it WITHOUT a stop (the job may keep running; the job file is left)`
4309
+ ) : j.job !== null ? ` ${what} on ${j.host}: stopped (its stop template) before anything else; a failed stop keeps everything` : ` ${what} on ${j.host}: released only if its carrier was never launched; otherwise the close refuses (then --abandon-job)`
4310
+ );
4311
+ }
4265
4312
  function printClosePreview(p) {
4266
4313
  if (p.kind === "none") {
4267
4314
  console.log(pc.dim(`no agent named ${p.agentName} (already closed?)`));
4268
4315
  return;
4269
4316
  }
4317
+ if (p.kind === "lease-job") {
4318
+ console.log(
4319
+ `${pc.bold(p.agentName)} has no agent row, only a lease job reservation (rev ${p.revision}): no pane is killed, no task changes`
4320
+ );
4321
+ printCarrierJob(p);
4322
+ return;
4323
+ }
4270
4324
  if (p.kind === "recovery" && p.recovery !== null && p.remote !== null) {
4271
4325
  const head = p.remote.collectedHead;
4272
4326
  console.log(
@@ -4275,6 +4329,7 @@ function printClosePreview(p) {
4275
4329
  console.log(
4276
4330
  ` remote worktree ${p.remote.path} on ${p.remote.host}: removed only if clean and at ${p.remote.base.slice(0, 12)}${head === null ? "" : ` or ${head.slice(0, 12)} (collected)`}; otherwise it refuses${p.keepBranch ? " (--keep-branch: unmerged commits go on a remote branch first; dirty still refuses)" : ""}`
4277
4331
  );
4332
+ printCarrierJob(p);
4278
4333
  if (p.recovery.refusal !== null)
4279
4334
  console.log(pc.yellow(` would refuse: ${p.recovery.refusal}`));
4280
4335
  return;
@@ -4282,6 +4337,7 @@ function printClosePreview(p) {
4282
4337
  console.log(
4283
4338
  p.kind === "agent" ? `Close ${pc.bold(p.agentName)}? pane ${p.paneId} will be killed and its row removed (rev ${p.revision})` : `${pc.bold(p.agentName)} has no agent row, only a registered worktree (rev ${p.revision})`
4284
4339
  );
4340
+ printCarrierJob(p);
4285
4341
  for (const t of p.ownedTasks) {
4286
4342
  console.log(` owns ${t.ref} (${t.status}${t.reverts ? ": goes back to OPEN" : ""})`);
4287
4343
  }
@@ -4316,6 +4372,9 @@ function wireCloseCommand(agent) {
4316
4372
  ).option(
4317
4373
  "--force",
4318
4374
  "close past what you vouch for: a pane mu cannot ask, or a worktree a process writes in or no scan can check (a cwd or reader there only warns); never overrides a live pane or uncommitted work"
4375
+ ).option(
4376
+ "--abandon-job",
4377
+ "a lease job whose host is gone for good: drop its handle WITHOUT stopping it (logged with host and handle; the job may keep running). --force never skips the stop"
4319
4378
  ).option("--if <rev>", "apply the preview with this revision", parseNonNegativeInt).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
4320
4379
  const opts = this.opts();
4321
4380
  return handle((db) => cmdClose(db, name, opts), this)();
@@ -4654,7 +4713,7 @@ async function parseWaitRef(db, raw, scope, legacy = {}) {
4654
4713
  return { kind, workstream, id: parsed.name };
4655
4714
  }
4656
4715
  if (getAgent(db, parsed.name, workstream) === void 0) {
4657
- const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-G5IVXSMY.js");
4716
+ const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-MDQZHB42.js");
4658
4717
  throw new AgentNotFoundError2(parsed.name);
4659
4718
  }
4660
4719
  return {
@@ -8045,7 +8104,7 @@ function openDashboardOrHelp(program2) {
8045
8104
  (syncDb) => withShutdown(syncDb, async (shutdown) => {
8046
8105
  const key = "NODE_ENV";
8047
8106
  process.env[key] ??= "production";
8048
- const { runDashboard } = await import("./tui-EN2CDP6G.js");
8107
+ const { runDashboard } = await import("./tui-5ZARE7RS.js");
8049
8108
  const first = program2.opts().workstream?.[0];
8050
8109
  await runDashboard({ dbPath: defaultDbPath(), syncDb, shutdown, initialWorkstream: first });
8051
8110
  }),
@@ -10170,6 +10229,31 @@ async function ctlSocketsDoctorCheck(agents, opts = {}) {
10170
10229
  }
10171
10230
  return { check, agents: reports };
10172
10231
  }
10232
+ function leaseJobHints(db) {
10233
+ const agentIds = new Set(
10234
+ db.prepare("SELECT id FROM agents").all().map((r) => r.id)
10235
+ );
10236
+ const named = db.prepare(
10237
+ `SELECT 1 FROM agents a JOIN workstreams w ON w.id = a.workstream_id
10238
+ WHERE w.name = ? AND a.name = ?`
10239
+ );
10240
+ return listCarrierJobs(db).filter(
10241
+ (j) => j.agentId === null ? named.get(j.workstream, j.name) === void 0 : !agentIds.has(j.agentId)
10242
+ ).map((j) => ({
10243
+ workstream: j.workstream,
10244
+ name: j.name,
10245
+ host: j.host,
10246
+ job: j.job,
10247
+ launch: j.launch,
10248
+ agentId: j.agentId,
10249
+ reason: j.reason,
10250
+ close: `mu agent close ${j.name} -w ${j.workstream}`
10251
+ }));
10252
+ }
10253
+ function leaseJobHintLine(h) {
10254
+ const what = h.job !== null ? `job ${h.job}` : `no job handle (launch ${h.launch}${h.reason === null ? "" : `, ${h.reason}`})`;
10255
+ return `${h.workstream}/${h.name} on ${h.host}: ${what}, agent ${h.agentId === null ? "never bound" : "gone"}: ${h.close}`;
10256
+ }
10173
10257
 
10174
10258
  // src/dormant.ts
10175
10259
  var FINISHED_IDLE_DAYS = 14;
@@ -10487,6 +10571,11 @@ state (workstream=${ws})`));
10487
10571
  console.log("");
10488
10572
  for (const line2 of oldDelegates.remediation) console.log(` ${line2}`);
10489
10573
  }
10574
+ const leaseJobs = leaseJobHints(db);
10575
+ console.log(
10576
+ ` ${"lease-job".padEnd(16)} : ${leaseJobs.length === 0 ? `${pc.green("ok")} ${pc.dim("no lease job reservation outlives its agent")}` : `${pc.yellow("hint")} ${pc.dim(`${leaseJobs.length} lease job reservation(s) whose agent is gone; each close stops its job (a host gone for good: --abandon-job)`)}`}`
10577
+ );
10578
+ for (const h of leaseJobs) console.log(pc.dim(` ${leaseJobHintLine(h)}`));
10490
10579
  console.log(pc.bold("\ndisk"));
10491
10580
  const disk = checkDiskRecon(db);
10492
10581
  if (printHazards(disk)) sawHazard = true;
@@ -10699,6 +10788,7 @@ async function cmdDoctorJson(db, opts = {}) {
10699
10788
  workstreams: findDormantWorkstreams(db, { currentWorkstream }),
10700
10789
  thresholds: { finishedIdleDays: FINISHED_IDLE_DAYS, abandonedIdleDays: ABANDONED_IDLE_DAYS }
10701
10790
  },
10791
+ leaseJobs: leaseJobHints(db),
10702
10792
  oldDelegates: (() => {
10703
10793
  const h = checkOldDelegates(db);
10704
10794
  return {
@@ -10812,7 +10902,9 @@ function cmdHostAdd(db, name, opts) {
10812
10902
  exec: opts.exec,
10813
10903
  fetch: opts.fetch,
10814
10904
  repo: opts.repo ?? null,
10815
- max_sessions: opts.maxSessions ?? null
10905
+ max_sessions: opts.maxSessions ?? null,
10906
+ transport: opts.transport ?? "session",
10907
+ stop: opts.stop ?? null
10816
10908
  });
10817
10909
  if (isJsonMode()) {
10818
10910
  emitJson({ host });
@@ -10832,9 +10924,15 @@ function cmdHostList(db) {
10832
10924
  }
10833
10925
  const held = /* @__PURE__ */ new Map();
10834
10926
  for (const c of listHostChannels(db)) held.set(c.host, (held.get(c.host) ?? 0) + 1);
10835
- const t = muTable({ head: ["HOST", "DEFAULT REPO", "CHANNELS"] });
10927
+ const t = muTable({ head: ["HOST", "TRANSPORT", "STOP", "DEFAULT REPO", "CHANNELS"] });
10836
10928
  for (const h of hosts) {
10837
- t.push([h.name, h.repo ?? "-", `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`]);
10929
+ t.push([
10930
+ h.name,
10931
+ h.transport,
10932
+ h.stop ?? "-",
10933
+ h.repo ?? "-",
10934
+ `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`
10935
+ ]);
10838
10936
  }
10839
10937
  console.log(t.toString());
10840
10938
  }
@@ -10846,9 +10944,10 @@ function cmdHostShow(db, name) {
10846
10944
  return;
10847
10945
  }
10848
10946
  console.log(
10849
- `${pc.bold(host.name)} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
10947
+ `${pc.bold(host.name)} transport ${host.transport} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
10850
10948
  );
10851
10949
  for (const f of ["shell", "exec", "fetch"]) console.log(` ${f.padEnd(6)} ${host[f]}`);
10950
+ if (host.stop !== null) console.log(` ${"stop".padEnd(6)} ${host.stop}`);
10852
10951
  console.log(
10853
10952
  channels.length === 0 ? " channels: none held" : ` channels: ${channels.map(channelLine).join("; ")}`
10854
10953
  );
@@ -10862,8 +10961,11 @@ function cmdHostRm(db, name) {
10862
10961
  console.log(removed ? `removed host ${name}` : `no host profile named ${name}`);
10863
10962
  }
10864
10963
  function cmdHostInit(name, opts) {
10865
- if (opts.ssh !== true) {
10866
- throw new UsageError("host init needs a starter kind: --ssh");
10964
+ if (opts.ssh === true && opts.mule === true) {
10965
+ throw new UsageError("host init takes one starter kind: --ssh or --mule");
10966
+ }
10967
+ if (opts.ssh !== true && opts.mule !== true) {
10968
+ throw new UsageError("host init needs a starter kind: --ssh or --mule");
10867
10969
  }
10868
10970
  if (!isValidHostName(name)) {
10869
10971
  throw new UsageError(
@@ -10871,20 +10973,24 @@ function cmdHostInit(name, opts) {
10871
10973
  { showHelp: false }
10872
10974
  );
10873
10975
  }
10874
- const s = hostStarter(name);
10976
+ const lease = opts.mule === true;
10977
+ const s = lease ? hostMuleStarter(name) : hostStarter(name);
10875
10978
  const command2 = [
10876
- `mu host add ${name}`,
10979
+ `mu host add ${name}${lease ? " --transport lease" : ""}`,
10877
10980
  `--shell ${shellQuote(s.shell)}`,
10878
10981
  `--exec ${shellQuote(s.exec)}`,
10982
+ ...s.stop === void 0 ? [] : [`--stop ${shellQuote(s.stop)}`],
10879
10983
  `--fetch ${shellQuote(s.fetch)}`,
10880
- "--max-sessions 1"
10984
+ ...lease ? [] : ["--max-sessions 1"]
10881
10985
  ].join(" \\\n ");
10882
10986
  if (isJsonMode()) {
10883
- emitJson({ host: name, ...s, command: command2 });
10987
+ emitJson({ host: name, ...lease ? { transport: "lease" } : {}, ...s, command: command2 });
10884
10988
  return;
10885
10989
  }
10886
10990
  console.log(`# Starter for ${name}. Edit it, then run it: mu runs none of this.`);
10887
- console.log(`# ${name} is the ssh target; ~/.ssh/config decides the rest.`);
10991
+ console.log(
10992
+ lease ? `# ${name} is mule's host; a lease worker holds no ssh session (docs/guide/remote.md).` : `# ${name} is the ssh target; ~/.ssh/config decides the rest.`
10993
+ );
10888
10994
  console.log(command2);
10889
10995
  }
10890
10996
  async function cmdHostCheck(db, name, opts) {
@@ -10902,6 +11008,7 @@ async function cmdHostCheck(db, name, opts) {
10902
11008
  const mark = s.ok === true ? pc.green("ok ") : s.ok === false ? pc.red("FAIL") : pc.dim("skip");
10903
11009
  console.log(`${mark} ${s.step.padEnd(9)} ${s.detail}`);
10904
11010
  }
11011
+ for (const w of report.warnings) console.log(`${pc.yellow("warn")} ${w}`);
10905
11012
  if (!report.ok) throw failedError(report);
10906
11013
  }
10907
11014
  function failedError(report) {
@@ -10915,10 +11022,16 @@ function wireHostCommands(program2) {
10915
11022
  "Host profiles: command templates for a remote host. mu never connects and knows no ssh flags; it fills your templates and runs them with /bin/sh -c"
10916
11023
  );
10917
11024
  host.command("add <name>").description(
10918
- "Store a machine-local host profile of command templates: --shell opens the pane, --exec runs one remote command, --fetch brings commits home. Placeholders {env} {cmd} {local_sock} {remote_sock} {cwd}; grammar in docs/guide/remote.md."
11025
+ "Store a machine-local host profile: --shell opens the pane, --exec runs one remote command, --fetch brings commits home. --transport lease adds --stop and {job_file}/{job}; grammar in docs/guide/remote.md."
10919
11026
  ).requiredOption("--shell <template>", "pane command template (opens the remote shell)").requiredOption("--exec <template>", "template that runs one remote command ({cmd})").requiredOption("--fetch <template>", "local template that fetches commits home ({cwd})").option(
10920
11027
  "--repo <path>",
10921
11028
  "default remote repo for `agent spawn --host` without --root (a profile names a machine, not a repo)"
11029
+ ).option(
11030
+ "--transport <kind>",
11031
+ "session (default: the pane holds an ssh) or lease (the pane is a carrier owning a detached remote job; needs --stop and {job_file} in --shell)"
11032
+ ).option(
11033
+ "--stop <template>",
11034
+ "lease only: ends a job given its handle ({job}, required; {cwd} is `.`). Must be idempotent: exit 0 for a job already ended"
10922
11035
  ).option(
10923
11036
  "--max-sessions <n>",
10924
11037
  "cap on channels mu holds to this host at once (panes, --exec, --fetch)",
@@ -10930,20 +11043,22 @@ function wireHostCommands(program2) {
10930
11043
  host.command("list").description("List host profiles with channels held / cap").option(...JSON_OPT).action(function() {
10931
11044
  return handle(async (db) => cmdHostList(db), this)();
10932
11045
  });
10933
- host.command("show <name>").description("Show a profile's templates and the channels it holds, with their holders").option(...JSON_OPT).action(function(name) {
11046
+ host.command("show <name>").description(
11047
+ "Show a profile's transport, templates and the channels it holds, with their holders"
11048
+ ).option(...JSON_OPT).action(function(name) {
10934
11049
  return handle(async (db) => cmdHostShow(db, name), this)();
10935
11050
  });
10936
11051
  host.command("rm <name>").description("Remove a profile. Refuses while it holds a channel (close those agents first)").option(...JSON_OPT).action(function(name) {
10937
11052
  return handle(async (db) => cmdHostRm(db, name), this)();
10938
11053
  });
10939
11054
  host.command("init <name>").description(
10940
- "Print a starter `mu host add` for ssh (the socket forward, `$SHELL -ilc`, identity env inside the command) for you to edit. Prints only: mu never runs it and never connects."
10941
- ).option("--ssh", "print the ssh starter").option(...JSON_OPT).action(function(name) {
11055
+ "Print a starter `mu host add` for you to edit: --ssh (the socket forward, `$SHELL -ilc`, identity env inside the command) or --mule (a lease profile for a session-capped host). Prints only: mu never runs it and never connects."
11056
+ ).option("--ssh", "print the ssh starter").option("--mule", "print the lease starter for a session-capped host (a mule carrier)").option(...JSON_OPT).action(function(name) {
10942
11057
  const opts = this.opts();
10943
11058
  return handle(async () => cmdHostInit(name, opts), this)();
10944
11059
  });
10945
11060
  host.command("check <name>").description(
10946
- "Run the profile's --exec under one channel: host answers, the repo (--root, else the default) is git, pi runs, `pi list` names mu. --agent also probes that agent's control socket. Exit 5 if a step fails; retries nothing."
11061
+ "Run the profile's --exec under one channel: host answers, repo is git, pi runs, `pi list` names mu. A lease profile's templates are checked too (stop expanded, not run). --agent probes its control socket and job. Exit 5 on failure."
10947
11062
  ).option("--agent <name>", "also probe this agent's control socket (the forward rides its pane)").option("--root <path>", "the remote repo the repo step checks (default: the profile's)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
10948
11063
  const opts = this.optsWithGlobals();
10949
11064
  return handle((db) => cmdHostCheck(db, name, opts), this)();
@@ -12472,4 +12587,4 @@ export {
12472
12587
  truncate,
12473
12588
  truncateFront
12474
12589
  };
12475
- //# sourceMappingURL=cli-H2PWJAJN.js.map
12590
+ //# sourceMappingURL=cli-H3VVCP7M.js.map