cyberlegion 1.10.0 → 2.0.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.
package/dist/cli.mjs CHANGED
@@ -8,7 +8,7 @@ import { stripVTControlCharacters } from "node:util";
8
8
  import { homedir } from "node:os";
9
9
  import { createHash, randomBytes, randomUUID } from "node:crypto";
10
10
  import { lstat, readFile, readdir, readlink, realpath, unlink, writeFile } from "node:fs/promises";
11
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/error.js
11
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/error.js
12
12
  /**
13
13
  * CommanderError class
14
14
  */
@@ -42,8 +42,7 @@ var InvalidArgumentError = class extends CommanderError {
42
42
  this.name = this.constructor.name;
43
43
  }
44
44
  };
45
- //#endregion
46
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/argument.js
45
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/argument.js
47
46
  var Argument = class {
48
47
  /**
49
48
  * Initialize a new command argument with the given name and description.
@@ -161,8 +160,7 @@ function humanReadableArgName(arg) {
161
160
  const nameOutput = arg.name() + (arg.variadic === true ? "..." : "");
162
161
  return arg.required ? "<" + nameOutput + ">" : "[" + nameOutput + "]";
163
162
  }
164
- //#endregion
165
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/help.js
163
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/help.js
166
164
  /**
167
165
  * TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`
168
166
  * https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types
@@ -230,8 +228,8 @@ var Help = class {
230
228
  const removeShort = helpOption.short && cmd._findOption(helpOption.short);
231
229
  const removeLong = helpOption.long && cmd._findOption(helpOption.long);
232
230
  if (!removeShort && !removeLong) visibleOptions.push(helpOption);
233
- else if (helpOption.long && !removeLong) visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description));
234
- else if (helpOption.short && !removeShort) visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description));
231
+ else if (helpOption.long && !removeLong) visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description).helpGroup(helpOption.helpGroupHeading));
232
+ else if (helpOption.short && !removeShort) visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description).helpGroup(helpOption.helpGroupHeading));
235
233
  }
236
234
  if (this.sortOptions) visibleOptions.sort(this.compareOptions);
237
235
  return visibleOptions;
@@ -636,8 +634,7 @@ var Help = class {
636
634
  return wrappedLines.join("\n");
637
635
  }
638
636
  };
639
- //#endregion
640
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/option.js
637
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/option.js
641
638
  var Option = class {
642
639
  /**
643
640
  * Initialize a new `Option` with the given `flags` and `description`.
@@ -650,7 +647,7 @@ var Option = class {
650
647
  this.description = description || "";
651
648
  this.required = flags.includes("<");
652
649
  this.optional = flags.includes("[");
653
- this.variadic = /\w\.\.\.[>\]]$/.test(flags);
650
+ this.variadic = /\p{ID_Continue}\.\.\.[>\]]$/u.test(flags);
654
651
  this.mandatory = false;
655
652
  const optionFlags = splitOptionFlags(flags);
656
653
  this.short = optionFlags.shortFlag;
@@ -934,8 +931,7 @@ function splitOptionFlags(flags) {
934
931
  longFlag
935
932
  };
936
933
  }
937
- //#endregion
938
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/suggestSimilar.js
934
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/suggestSimilar.js
939
935
  const maxDistance = 3;
940
936
  function editDistance(a, b) {
941
937
  if (Math.abs(a.length - b.length) > maxDistance) return Math.max(a.length, b.length);
@@ -986,8 +982,7 @@ function suggestSimilar(word, candidates) {
986
982
  if (similar.length === 1) return `\n(Did you mean ${similar[0]}?)`;
987
983
  return "";
988
984
  }
989
- //#endregion
990
- //#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/command.js
985
+ // ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/command.js
991
986
  var Command = class Command extends EventEmitter {
992
987
  /**
993
988
  * Initialize a new `Command`.
@@ -1737,7 +1732,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1737
1732
  getOptionValueSourceWithGlobals(key) {
1738
1733
  let source;
1739
1734
  this._getCommandAndAncestors().forEach((cmd) => {
1740
- if (cmd.getOptionValueSource(key) !== void 0) source = cmd.getOptionValueSource(key);
1735
+ if (Object.hasOwn(cmd._optionValueSources, key)) source = cmd.getOptionValueSource(key);
1741
1736
  });
1742
1737
  return source;
1743
1738
  }
@@ -2246,7 +2241,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2246
2241
  return arg.length > 1 && arg[0] === "-";
2247
2242
  }
2248
2243
  const negativeNumberArg = (arg) => {
2249
- if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg)) return false;
2244
+ if (!/^-(\d+|\d*\.\d+)([eE][+-]?\d+)?$/.test(arg)) return false;
2250
2245
  return !this._getCommandAndAncestors().some((cmd) => cmd.options.map((opt) => opt.short).some((short) => /^-\d$/.test(short)));
2251
2246
  };
2252
2247
  let activeVariadicOption = null;
@@ -2646,7 +2641,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2646
2641
  * (This does not override a group set directly on the subcommand using .helpGroup().)
2647
2642
  *
2648
2643
  * @example
2649
- * program.commandsGroup('Development Commands:);
2644
+ * program.commandsGroup('Development Commands:');
2650
2645
  * program.command('watch')...
2651
2646
  * program.command('lint')...
2652
2647
  * ...
@@ -2815,7 +2810,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2815
2810
  * Pass in false to disable the built-in help option.
2816
2811
  *
2817
2812
  * @example
2818
- * program.helpOption('-?, --help' 'show help'); // customise
2813
+ * program.helpOption('-?, --help', 'show help'); // customise
2819
2814
  * program.helpOption(false); // disable
2820
2815
  *
2821
2816
  * @param {(string | boolean)} flags
@@ -2937,12 +2932,12 @@ function incrementNodeInspectorPort(args) {
2937
2932
  let debugHost = "127.0.0.1";
2938
2933
  let debugPort = "9229";
2939
2934
  let match;
2940
- if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) debugOption = match[1];
2941
- else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) {
2935
+ if ((match = arg.match(/^(--inspect(-brk|-wait)?)$/)) !== null) debugOption = match[1];
2936
+ else if ((match = arg.match(/^(--inspect(-brk|-port|-wait)?)=(\[[^\]]+\]|[^:]+)$/)) !== null) {
2942
2937
  debugOption = match[1];
2943
2938
  if (/^\d+$/.test(match[3])) debugPort = match[3];
2944
2939
  else debugHost = match[3];
2945
- } else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) {
2940
+ } else if ((match = arg.match(/^(--inspect(-brk|-port|-wait)?)=(\[[^\]]+\]|[^:]+):(\d+)$/)) !== null) {
2946
2941
  debugOption = match[1];
2947
2942
  debugHost = match[3];
2948
2943
  debugPort = match[4];
@@ -2962,8 +2957,7 @@ function useColor() {
2962
2957
  if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
2963
2958
  }
2964
2959
  new Command();
2965
- //#endregion
2966
- //#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/exec-Bmdcg_T7.mjs
2960
+ // ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/exec-Bmdcg_T7.mjs
2967
2961
  const nodeExec$1 = (cmd, args) => {
2968
2962
  try {
2969
2963
  const out = execFileSync(cmd, args, {
@@ -2990,8 +2984,7 @@ const nodeExec$1 = (cmd, args) => {
2990
2984
  function withReason(exec, message) {
2991
2985
  return exec.lastError ? `${message} — ${exec.lastError}` : message;
2992
2986
  }
2993
- //#endregion
2994
- //#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/backend-D3VSsev4.mjs
2987
+ // ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/backend-D3VSsev4.mjs
2995
2988
  const KNOWN_MUX = [
2996
2989
  "tmux",
2997
2990
  "rmux",
@@ -9138,8 +9131,7 @@ function callerPane(adapter, env) {
9138
9131
  const self = currentPane(env);
9139
9132
  return self && self.mux === adapter.name ? { id: self.pane } : void 0;
9140
9133
  }
9141
- //#endregion
9142
- //#region src/admin.ts
9134
+ // src/admin.ts
9143
9135
  /**
9144
9136
  * Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
9145
9137
  * the destination). Used by `admin migrate` when moving state between an old project-local root
@@ -9173,8 +9165,7 @@ function migrateStore(from, to) {
9173
9165
  briefs
9174
9166
  };
9175
9167
  }
9176
- //#endregion
9177
- //#region ../../node_modules/.pnpm/cyber-agentkit@2.0.0/node_modules/cyber-agentkit/dist/worktrees.js
9168
+ // ../../node_modules/.pnpm/cyber-agentkit@2.2.0/node_modules/cyber-agentkit/dist/worktrees.js
9178
9169
  /**
9179
9170
  * Whether a checkout has uncommitted changes, tracked or untracked, or `undefined` when git could not
9180
9171
  * say. `undefined` is not `false`: a caller deciding whether a worktree is safe to reuse must not read
@@ -10247,8 +10238,7 @@ async function exists(path) {
10247
10238
  return false;
10248
10239
  }
10249
10240
  }
10250
- //#endregion
10251
- //#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/worktree.mjs
10241
+ // ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/worktree.mjs
10252
10242
  /**
10253
10243
  * This module's own refusals and failures — plain cyber-mux prose, never a dependency's raw words.
10254
10244
  * `reportWorktreeFailure` (`cli.ts`) forwards a `WorktreeGitError`'s message onto stdout verbatim
@@ -10309,8 +10299,7 @@ function resolvePrimaryRoot(exec) {
10309
10299
  function assertDistinctFromPrimary(worktreeRoot, primaryRoot) {
10310
10300
  if (resolve(worktreeRoot) === resolve(primaryRoot)) throw new WorktreeGitError("refusing to run in the primary checkout — spawn a worktree distinct from the primary checkout");
10311
10301
  }
10312
- //#endregion
10313
- //#region src/paths.ts
10302
+ // src/paths.ts
10314
10303
  /** The tracked marker file that makes a hub root initialized (see ensureMarker). */
10315
10304
  const MARKER_FILE = "config.json";
10316
10305
  /** Where `spawn` stamps that marker inside a worktree it creates, relative to the worktree root. */
@@ -10406,8 +10395,7 @@ const paths = {
10406
10395
  function sanitizePane(pane) {
10407
10396
  return pane.replace(/[^A-Za-z0-9_-]/g, "_");
10408
10397
  }
10409
- //#endregion
10410
- //#region src/mux-env.ts
10398
+ // src/mux-env.ts
10411
10399
  /**
10412
10400
  * Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
10413
10401
  * is still honored"). cyber-mux's own fast-path reads `$CYBER_MUX`/`$CYBER_MUX_PANE` — its
@@ -10436,8 +10424,7 @@ function normalizeMuxEnv(env) {
10436
10424
  CYBER_MUX_PANE: env.CYBERLEGION_MUX_PANE
10437
10425
  };
10438
10426
  }
10439
- //#endregion
10440
- //#region src/identity.ts
10427
+ // src/identity.ts
10441
10428
  const realExec = nodeExec$1;
10442
10429
  /** `currentPane`, narrowed to a backend the registry can actually store a locator under. A caller
10443
10430
  * inside a pane-carrying-but-unstorable multiplexer (wezterm/zellij) is simply unpaned here — it can
@@ -10450,6 +10437,133 @@ function storablePane(env) {
10450
10437
  pane: cur.pane
10451
10438
  } : void 0;
10452
10439
  }
10440
+ /** The calling session's storable pane, resolved the way `register` resolves it: herdr's answer
10441
+ * when herdr gives a trustworthy one, `$HERDR_PANE_ID` otherwise (#154, #268). */
10442
+ function callingStorablePane(ctx) {
10443
+ return selfPane(storablePane(ctx.env ?? process.env), ctx.exec ?? realExec);
10444
+ }
10445
+ /**
10446
+ * This session's own pane as the multiplexer sees it now. The env names the pane the shell was
10447
+ * launched in, and for herdr that goes stale: herdr gives a pane a new id when it moves to another
10448
+ * tab or workspace, while the shell keeps its launch-time `$HERDR_PANE_ID`, and herdr ids recur on a
10449
+ * hub, so the stale id can be another unit's pane (#154). herdr's `pane current` resolves the
10450
+ * calling pane itself, so its answer wins on a herdr that answers it truly (0.8.2 on). When herdr
10451
+ * gives no answer, or an older herdr disagrees with the env, the env id stands. tmux pane
10452
+ * ids last as long as the pane, so tmux needs no second opinion.
10453
+ */
10454
+ function selfPane(cur, exec) {
10455
+ if (cur?.mux !== "herdr") return cur;
10456
+ const live = herdrCallingPane(exec);
10457
+ if (!live) return cur;
10458
+ if (live.pane === cur.pane) return {
10459
+ ...cur,
10460
+ ...live.terminal ? { terminal: live.terminal } : {}
10461
+ };
10462
+ if (!herdrReportsCallingPane(exec)) return cur;
10463
+ return {
10464
+ ...cur,
10465
+ pane: live.pane,
10466
+ ...live.terminal ? { terminal: live.terminal } : {}
10467
+ };
10468
+ }
10469
+ /** herdr 0.8.2 fixed `pane current` to answer the calling pane; before it, the command answered
10470
+ * another client's focused pane (herdr #2297), which would misattribute exactly as a stale env id
10471
+ * does. A version that cannot be read is treated as too old. */
10472
+ function herdrReportsCallingPane(exec) {
10473
+ const m = exec("herdr", ["--version"])?.match(/(\d+)\.(\d+)\.(\d+)/);
10474
+ if (!m) return false;
10475
+ const [major, minor, patch] = m.slice(1).map(Number);
10476
+ return major > 0 || minor > 8 || minor === 8 && patch >= 2;
10477
+ }
10478
+ function herdrCallingPane(exec) {
10479
+ return herdrPaneAnswer(exec("herdr", ["pane", "current"]));
10480
+ }
10481
+ /** Parse a herdr `pane current` / `pane get` answer into its pane id and terminal id. */
10482
+ function herdrPaneAnswer(out) {
10483
+ if (!out) return void 0;
10484
+ try {
10485
+ const pane = JSON.parse(out)?.result?.pane;
10486
+ const id = pane?.pane_id;
10487
+ if (typeof id !== "string" || !id) return void 0;
10488
+ const terminal = pane.terminal_id;
10489
+ return {
10490
+ pane: id,
10491
+ ...typeof terminal === "string" && terminal ? { terminal } : {}
10492
+ };
10493
+ } catch {
10494
+ return;
10495
+ }
10496
+ }
10497
+ /**
10498
+ * The herdr terminal id behind a pane id — what a unit's herdr pane record keys on besides the pane
10499
+ * id. herdr gives a pane a new id when it moves to another workspace and keeps its terminal id
10500
+ * (#272; herdr 0.9.5: `pane move wRD:p2 --tab wRE:t1` answered `wRE:p2` on the same `terminal_id`).
10501
+ * Undefined when herdr does not answer or reports no terminal id.
10502
+ */
10503
+ function herdrTerminalOf(exec, paneId) {
10504
+ return herdrPaneAnswer(exec("herdr", [
10505
+ "pane",
10506
+ "get",
10507
+ paneId
10508
+ ]))?.terminal;
10509
+ }
10510
+ /** The pane locator for a pane a spawn or restart just opened: tagged with its multiplexer so the
10511
+ * unit's own `prune` runs the right liveness check, and for herdr carrying the terminal id a move
10512
+ * keeps. `null` for a backend a record cannot carry. */
10513
+ function openedPaneLocator(exec, mux, id) {
10514
+ if (mux === "tmux") return {
10515
+ mux,
10516
+ id
10517
+ };
10518
+ if (mux !== "herdr") return null;
10519
+ const terminal = herdrTerminalOf(exec, id);
10520
+ return {
10521
+ mux,
10522
+ id,
10523
+ ...terminal ? { terminal } : {}
10524
+ };
10525
+ }
10526
+ /** herdr's live panes keyed by terminal id → current pane id. Empty when herdr gives no list, which
10527
+ * says nothing about any pane. */
10528
+ function herdrTerminals(exec) {
10529
+ const map = /* @__PURE__ */ new Map();
10530
+ const out = exec("herdr", ["pane", "list"]);
10531
+ if (!out) return map;
10532
+ try {
10533
+ const panes = JSON.parse(out)?.result?.panes;
10534
+ if (!Array.isArray(panes)) return map;
10535
+ for (const p of panes) if (typeof p?.terminal_id === "string" && typeof p?.pane_id === "string") map.set(p.terminal_id, p.pane_id);
10536
+ } catch {
10537
+ return map;
10538
+ }
10539
+ return map;
10540
+ }
10541
+ /**
10542
+ * Where a herdr record that carries its terminal id lives now: the pane id herdr lists for that
10543
+ * terminal (`{ id }`), `'gone'` when herdr answered with a list that lacks the terminal, or
10544
+ * `undefined` when herdr gave no list. Not called for a record without a terminal id, which keeps
10545
+ * being judged by its pane id.
10546
+ */
10547
+ function locateTerminal(exec, terminal, cache = {}) {
10548
+ cache.terminals ??= herdrTerminals(exec);
10549
+ if (cache.terminals.size === 0) return void 0;
10550
+ const id = cache.terminals.get(terminal);
10551
+ return id ? { id } : "gone";
10552
+ }
10553
+ /** Point a record whose herdr pane moved at the pane id it has now, moving its pane pointer with it.
10554
+ * The old pointer is dropped only while it still names this record, since herdr hands old ids out
10555
+ * again. */
10556
+ function followMove(ctx, rec, id) {
10557
+ const pane = rec.pane;
10558
+ if (!pane || pane.id === id) return;
10559
+ if (ctx.store.resolvePaneId(pane.id) === rec.id) ctx.store.removePaneIndex(pane.id);
10560
+ rec.pane = {
10561
+ ...pane,
10562
+ id
10563
+ };
10564
+ saveAgent(ctx.store, rec);
10565
+ ctx.store.putPaneIndex(id, rec.id);
10566
+ }
10453
10567
  const nowIso = (ctx) => new Date(ctx.now?.() ?? Date.now()).toISOString();
10454
10568
  function randomId() {
10455
10569
  return randomBytes(8).toString("hex");
@@ -10495,10 +10609,33 @@ function detectHarness(explicit, ctx) {
10495
10609
  * explicit via the env var.
10496
10610
  */
10497
10611
  function resolveSelfId(ctx) {
10612
+ return resolveSelf(ctx).id;
10613
+ }
10614
+ function resolveSelf(ctx) {
10498
10615
  const env = ctx.env ?? process.env;
10499
- const cur = currentPane(normalizeMuxEnv(env));
10500
- if (cur) return ctx.store.resolvePaneId(cur.pane);
10501
- return env.CYBERLEGION_AGENT_ID || void 0;
10616
+ const cur = selfPane(currentPane(normalizeMuxEnv(env)), ctx.exec ?? realExec);
10617
+ if (cur) return {
10618
+ id: selfIdAt(ctx.store, cur),
10619
+ cur
10620
+ };
10621
+ return {
10622
+ id: env.CYBERLEGION_AGENT_ID || void 0,
10623
+ cur
10624
+ };
10625
+ }
10626
+ /**
10627
+ * The unit a pane belongs to. The pane pointer answers first. When herdr named the calling pane's
10628
+ * terminal, a pointer whose record carries a different terminal is stale (the id was handed to
10629
+ * this pane after the unit's own pane moved away), and a missing pointer may mean this pane moved:
10630
+ * either way the record bearing this terminal is the caller (#272). A record without a terminal id
10631
+ * is trusted by its pointer as before.
10632
+ */
10633
+ function selfIdAt(store, cur) {
10634
+ const indexed = store.resolvePaneId(cur.pane);
10635
+ if (!cur.terminal) return indexed;
10636
+ const terminal = indexed ? loadAgent(store, indexed)?.pane?.terminal : void 0;
10637
+ if (indexed && (!terminal || terminal === cur.terminal)) return indexed;
10638
+ return listAgents(store).find((a) => a.pane?.mux === "herdr" && a.pane.terminal === cur.terminal)?.id;
10502
10639
  }
10503
10640
  /** Register (or idempotently refresh) this session's identity. */
10504
10641
  function register(ctx, input) {
@@ -10510,8 +10647,8 @@ function register(ctx, input) {
10510
10647
  const existing = existingId ? loadAgent(ctx.store, existingId) : void 0;
10511
10648
  const id = existing?.id ?? existingId ?? randomId();
10512
10649
  const ts = nowIso(ctx);
10513
- const cur = storablePane(env);
10514
10650
  const exec = ctx.exec ?? realExec;
10651
+ const cur = selfPane(storablePane(env), exec);
10515
10652
  const rec = {
10516
10653
  id,
10517
10654
  handle: input.handle ?? existing?.handle ?? id.slice(0, 6),
@@ -10546,7 +10683,8 @@ function paneLocator(cur, exec, env) {
10546
10683
  };
10547
10684
  return {
10548
10685
  mux: cur.mux,
10549
- id: cur.pane
10686
+ id: cur.pane,
10687
+ ...cur.terminal ? { terminal: cur.terminal } : {}
10550
10688
  };
10551
10689
  }
10552
10690
  /** Derive a standing record's stable id from its handle — same slug rule as `sanitizePane`, prefixed
@@ -10855,10 +10993,26 @@ function bumpLastSeen(ctx, id) {
10855
10993
  rec.lastSeen = nowIso(ctx);
10856
10994
  saveAgent(ctx.store, rec);
10857
10995
  }
10858
- /** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
10996
+ /**
10997
+ * Refresh this session's own last-seen if it is registered — best-effort, never throws. In a herdr
10998
+ * pane whose calling pane herdr reported, it also heals the record's pane to the id and terminal
10999
+ * herdr gave, so a unit whose pane moved fixes its own record on its next turn (#272).
11000
+ */
10859
11001
  function touch(ctx) {
10860
- const id = resolveSelfId(ctx);
10861
- if (id) bumpLastSeen(ctx, id);
11002
+ const { id, cur } = resolveSelf(ctx);
11003
+ if (!id) return;
11004
+ bumpLastSeen(ctx, id);
11005
+ if (cur?.mux !== "herdr" || !cur.terminal) return;
11006
+ const rec = loadAgent(ctx.store, id);
11007
+ if (rec?.pane?.mux !== "herdr") return;
11008
+ if (rec.pane.terminal !== cur.terminal) {
11009
+ rec.pane = {
11010
+ ...rec.pane,
11011
+ terminal: cur.terminal
11012
+ };
11013
+ saveAgent(ctx.store, rec);
11014
+ }
11015
+ followMove(ctx, rec, cur.pane);
10862
11016
  }
10863
11017
  const STALE_MS = 9e5;
10864
11018
  /** Standing and service records have no session of their own, so nothing about a pane or a timer
@@ -10885,7 +11039,9 @@ function isPrunable(rec) {
10885
11039
  function sessionLive(ctx, rec) {
10886
11040
  if (rec.status === "exited" || rec.status === "stopped") return false;
10887
11041
  if (!rec.pane) return true;
10888
- const panes = PANE_ADAPTERS[rec.pane.mux].listPanes(ctx.exec ?? realExec);
11042
+ const exec = ctx.exec ?? realExec;
11043
+ if (rec.pane.mux === "herdr" && rec.pane.terminal) return locateTerminal(exec, rec.pane.terminal) !== "gone";
11044
+ const panes = PANE_ADAPTERS[rec.pane.mux].listPanes(exec);
10889
11045
  if (panes.length === 0) return true;
10890
11046
  const id = rec.pane.id;
10891
11047
  return panes.some((p) => p.id === id);
@@ -10960,11 +11116,12 @@ function reconcile(ctx, opts) {
10960
11116
  if (panes.length === 0) return [];
10961
11117
  const live = new Set(panes.map((p) => p.id));
10962
11118
  const changed = [];
11119
+ const cache = {};
10963
11120
  for (const rec of listAgents(ctx.store)) {
10964
11121
  if (!isPrunable(rec)) continue;
10965
11122
  if (!rec.pane) continue;
10966
11123
  if (rec.pane.mux !== cur.mux) continue;
10967
- if (!live.has(rec.pane.id)) {
11124
+ if (rec.pane.mux === "herdr" && rec.pane.terminal ? terminalGone(ctx, rec, rec.pane.terminal, cache) : !live.has(rec.pane.id)) {
10968
11125
  rec.status = "exited";
10969
11126
  saveAgent(ctx.store, rec);
10970
11127
  changed.push(rec);
@@ -10989,27 +11146,111 @@ function paneGone(exec, pane, lists) {
10989
11146
  }
10990
11147
  return panes.length > 0 && !panes.some((p) => p.id === pane.id);
10991
11148
  }
10992
- /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
10993
- * first, then judges each remaining record (covers the other mux and sessions outside any
10994
- * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
10995
- * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
10996
- function prune(ctx) {
11149
+ /**
11150
+ * Whether a herdr record that carries its terminal id is positively gone: herdr listed its panes and
11151
+ * that terminal is not among them. A live terminal under a new pane id is a moved pane, so the record
11152
+ * follows it instead (#272). A pane id that herdr now gives another terminal does not keep the record
11153
+ * alive, since herdr ids recur. No list at all decides nothing.
11154
+ */
11155
+ function terminalGone(ctx, rec, terminal, cache) {
11156
+ const at = locateTerminal(ctx.exec ?? realExec, terminal, cache);
11157
+ if (at === "gone") return true;
11158
+ if (at) followMove(ctx, rec, at.id);
11159
+ return false;
11160
+ }
11161
+ /**
11162
+ * Mark agents whose session is gone as exited, then delete every exited unit with nothing left to
11163
+ * work from. Reconcile-culls against the current mux's live set first, then judges each remaining
11164
+ * record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
11165
+ * pane alone — a live or unprobeable pane keeps it whatever its last-seen — and a pane-less record by
11166
+ * the staleness timer, its only signal. An exited unit whose own linked worktree is still on disk is
11167
+ * kept, since `unit close` owns removing that worktree. So is an exited unit that is the last record
11168
+ * pointing at a linked worktree it shares, while that worktree is on disk and its work has not landed
11169
+ * (`options.landed`): deleting it would leave that work with nothing pointing at it. Every other
11170
+ * exited unit is deleted with `deleteExited`.
11171
+ */
11172
+ function prune(ctx, options = {}) {
10997
11173
  const exec = ctx.exec ?? realExec;
10998
11174
  const now = ctx.now?.() ?? Date.now();
10999
- const changed = reconcile(ctx);
11175
+ const marked = reconcile(ctx);
11000
11176
  const lists = /* @__PURE__ */ new Map();
11177
+ const cache = {};
11001
11178
  for (const rec of listAgents(ctx.store)) {
11002
11179
  if (!isPrunable(rec)) continue;
11003
- if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
11180
+ if (!rec.pane ? now - new Date(rec.lastSeen).getTime() > STALE_MS : rec.pane.mux === "herdr" && rec.pane.terminal ? terminalGone(ctx, rec, rec.pane.terminal, cache) : paneGone(exec, rec.pane, lists)) {
11004
11181
  rec.status = "exited";
11005
11182
  saveAgent(ctx.store, rec);
11006
- changed.push(rec);
11007
- }
11183
+ marked.push(rec);
11184
+ }
11185
+ }
11186
+ const deleted = deleteExited(ctx, options.landed ?? (() => false));
11187
+ const gone = new Set(deleted.map((r) => r.id));
11188
+ return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
11189
+ ...r,
11190
+ pruned: "exited"
11191
+ })), ...deleted.map((r) => ({
11192
+ ...r,
11193
+ pruned: "deleted"
11194
+ }))];
11195
+ }
11196
+ /** Whether `checkout` is a linked worktree still on disk — something a record can point at. The
11197
+ * primary checkout always exists, but nothing removes it, so no record has to stay to point at it. */
11198
+ function linkedOnDisk(checkout) {
11199
+ return checkout?.root != null && existsSync(checkout.root) && !isPrimaryCheckout(checkout.root, checkout.primaryRoot);
11200
+ }
11201
+ /** Whether another record in the store still points at the worktree at `root`, owned or shared. */
11202
+ function pointedAtByOther(store, root, self) {
11203
+ const at = resolve(root);
11204
+ return listAgents(store).some((a) => a.id !== self && (a.worktree?.root != null && resolve(a.worktree.root) === at || a.sharedWorktree?.root != null && resolve(a.sharedWorktree.root) === at));
11205
+ }
11206
+ /**
11207
+ * Whether deleting this exited record would orphan a worktree on disk. Its own linked worktree is kept
11208
+ * for `unit close` to remove. A worktree it only shares is kept while this record is the last one
11209
+ * pointing at it and its work has not landed — the owner's record is gone, so nothing else would
11210
+ * lead to that work.
11211
+ */
11212
+ function keepsWorktree(store, rec, landed) {
11213
+ if (linkedOnDisk(rec.worktree)) return true;
11214
+ const shared = rec.sharedWorktree;
11215
+ return linkedOnDisk(shared) && !pointedAtByOther(store, shared.root, rec.id) && !landed(shared.root);
11216
+ }
11217
+ /** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
11218
+ * `unit prune` share for "a seat on the main checkout, which nothing removes". */
11219
+ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
11220
+ return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
11221
+ }
11222
+ /**
11223
+ * Delete every exited unit that leaves no work on disk (`keepsWorktree`): its record, brief, mailbox, and the pane
11224
+ * pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
11225
+ * stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
11226
+ * resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
11227
+ * since. A worktree that is gone from disk may still be registered with git, and the record is the
11228
+ * last place that names its repository, so that registration is cleared first, best-effort, as
11229
+ * `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
11230
+ * someone else.
11231
+ */
11232
+ function deleteExited(ctx, landed) {
11233
+ const exec = ctx.exec ?? realExec;
11234
+ const deleted = [];
11235
+ for (const rec of listAgents(ctx.store)) {
11236
+ if (rec.status !== "exited" || isSessionIndependent(rec) || keepsWorktree(ctx.store, rec, landed)) continue;
11237
+ const primaryRoot = rec.worktree?.primaryRoot;
11238
+ if (primaryRoot && existsSync(primaryRoot)) exec("git", [
11239
+ "-C",
11240
+ primaryRoot,
11241
+ "worktree",
11242
+ "prune"
11243
+ ]);
11244
+ const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(rec.id);
11245
+ if (pane && ctx.store.resolvePaneId(pane) === rec.id) ctx.store.removePaneIndex(pane);
11246
+ ctx.store.removeAgent(rec.id);
11247
+ ctx.store.removeAgentData(rec.id);
11248
+ ctx.store.removeMailbox(rec.id);
11249
+ deleted.push(rec);
11008
11250
  }
11009
- return changed;
11251
+ return deleted;
11010
11252
  }
11011
- //#endregion
11012
- //#region src/console/prompt-guard.ts
11253
+ // src/console/prompt-guard.ts
11013
11254
  const CODEX_PLACEHOLDERS = /* @__PURE__ */ new Set([
11014
11255
  "Ask Codex to do anything",
11015
11256
  "Explain this codebase",
@@ -11167,8 +11408,7 @@ async function clearLine(adapter, exec, target, sleep) {
11167
11408
  adapter.sendText(exec, target, KILL_LINE);
11168
11409
  await sleep(KEY_SETTLE_MS);
11169
11410
  }
11170
- //#endregion
11171
- //#region src/console/ring.ts
11411
+ // src/console/ring.ts
11172
11412
  const DEFAULT_ATTEMPTS = 10;
11173
11413
  const DEFAULT_SETTLE_MS = 400;
11174
11414
  /** cyber-mux's `isStaged` matches on the same prefix length. */
@@ -11273,8 +11513,7 @@ async function ringTurn(adapter, exec, target, message, opts = {}) {
11273
11513
  const why = state === "lost" ? "the text vanished without the harness posting it" : `input still staged after ${attempts} re-submit attempts`;
11274
11514
  throw new Error(`nudge failed: peer at pane ${target.id} never took the turn — ${why}`);
11275
11515
  }
11276
- //#endregion
11277
- //#region src/console/doorbell.ts
11516
+ // src/console/doorbell.ts
11278
11517
  /** The doorbell text delivered to a woken recipient; also the standalone `unit nudge` default. */
11279
11518
  const DELIVERY_DOORBELL = "You have unread mail — check your inbox.";
11280
11519
  /**
@@ -11473,8 +11712,7 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
11473
11712
  };
11474
11713
  }
11475
11714
  }
11476
- //#endregion
11477
- //#region src/console/trust.ts
11715
+ // src/console/trust.ts
11478
11716
  /**
11479
11717
  * Each harness's folder-trust prompt, as it draws it, and the keys that accept it. Read from Claude
11480
11718
  * Code 2.1.286, Codex 0.153.4 (and its newer "Trust this folder?" wording), and cursor-agent
@@ -11584,8 +11822,7 @@ function stuck(where) {
11584
11822
  message: `${where}, and the prompt is still showing after spawn sent the keys that accept it. A person must answer it.`
11585
11823
  };
11586
11824
  }
11587
- //#endregion
11588
- //#region src/mux-select.ts
11825
+ // src/mux-select.ts
11589
11826
  /**
11590
11827
  * Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
11591
11828
  * `$CYBERLEGION_MUX*` → `$CYBER_MUX*` env seam (`mux-env.ts`) — tmux/herdr map to their existing
@@ -11605,8 +11842,7 @@ function selectSessionAdapter(env, exec = realExec) {
11605
11842
  if (probe.mux === "wezterm" || probe.mux === "zellij") throw new Error(`spawn detected ${probe.mux}, a backend unit/registry cannot store a pane locator under (only tmux and herdr) — refusing before opening anything`);
11606
11843
  throw new Error("spawn requires a session backend — run inside tmux ($TMUX) or herdr ($HERDR_ENV=1)");
11607
11844
  }
11608
- //#endregion
11609
- //#region src/workspace-label.ts
11845
+ // src/workspace-label.ts
11610
11846
  /** Leading actions that tear down or revert — the `A2-` class, checked first. */
11611
11847
  const TEARDOWN_ACTIONS = /* @__PURE__ */ new Set([
11612
11848
  "clean",
@@ -11749,8 +11985,7 @@ function fitWords(words, budget) {
11749
11985
  }
11750
11986
  return out;
11751
11987
  }
11752
- //#endregion
11753
- //#region src/worktree-lease.ts
11988
+ // src/worktree-lease.ts
11754
11989
  /**
11755
11990
  * The two worktree-library calls a unit's lifetime makes: `unit spawn` acquires a worktree, `unit
11756
11991
  * close` releases it. Gathered here so a test can stand in for them and drive spawn's routing without
@@ -11774,8 +12009,7 @@ function leaseHolder(id) {
11774
12009
  * cyberlegion's own file, not the unit's work, and counting it would keep every slot from reuse in a
11775
12010
  * project that does not track it. */
11776
12011
  const LEASE_IGNORE = [WORKTREE_MARKER];
11777
- //#endregion
11778
- //#region src/session.ts
12012
+ // src/session.ts
11779
12013
  /** How each harness's own CLI is launched in the new pane. */
11780
12014
  const LAUNCH_MAP = {
11781
12015
  claude: "claude",
@@ -12000,10 +12234,7 @@ async function spawn(ctx, input) {
12000
12234
  cwd,
12001
12235
  worktree,
12002
12236
  ...sharedWorktree ? { sharedWorktree } : {},
12003
- pane: muxName === "tmux" || muxName === "herdr" ? {
12004
- mux: muxName,
12005
- id: target.id
12006
- } : null,
12237
+ pane: openedPaneLocator(exec, muxName, target.id),
12007
12238
  status: "active",
12008
12239
  createdAt: ts,
12009
12240
  lastSeen: ts,
@@ -12325,8 +12556,7 @@ function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
12325
12556
  if (input.task != null && input.task !== "") return input.task;
12326
12557
  return null;
12327
12558
  }
12328
- //#endregion
12329
- //#region src/agentdef/resolve.ts
12559
+ // src/agentdef/resolve.ts
12330
12560
  function parseAgentDefFile(text) {
12331
12561
  const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
12332
12562
  if (!m) return {
@@ -12446,8 +12676,7 @@ function listAgentDefs(input = {}) {
12446
12676
  }
12447
12677
  return defs;
12448
12678
  }
12449
- //#endregion
12450
- //#region src/agentdef/realize.ts
12679
+ // src/agentdef/realize.ts
12451
12680
  const DEFAULT_HARNESS = "claude";
12452
12681
  /** POSIX single-quote a value for safe inclusion in a shell command line. */
12453
12682
  function shellQuote(value) {
@@ -12565,8 +12794,7 @@ function resolveSpawnLaunch(input) {
12565
12794
  ...input.searchRoots ? { searchRoots: input.searchRoots } : {}
12566
12795
  }), overrides);
12567
12796
  }
12568
- //#endregion
12569
- //#region src/cli-input.ts
12797
+ // src/cli-input.ts
12570
12798
  /**
12571
12799
  * Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
12572
12800
  * `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
@@ -12614,8 +12842,7 @@ function spawnCommandInput(opts, listCursorModels) {
12614
12842
  function readCommandOutput(format, result) {
12615
12843
  return format === "json" ? JSON.stringify(result, null, 2) : result.output;
12616
12844
  }
12617
- //#endregion
12618
- //#region src/cursor-models.ts
12845
+ // src/cursor-models.ts
12619
12846
  /** The model ids in `cursor-agent models` output: the first token of each line, ANSI colour and list
12620
12847
  * bullets stripped. Headings and tips come along too; callers only test a specific id for membership. */
12621
12848
  function parseCursorModels(output) {
@@ -12638,8 +12865,7 @@ function listCursorModels() {
12638
12865
  return [];
12639
12866
  }
12640
12867
  }
12641
- //#endregion
12642
- //#region src/message.ts
12868
+ // src/message.ts
12643
12869
  /** Write one message into the recipient's inbox. Collision-free by <epochMs>-<hex>. */
12644
12870
  function send(ctx, input) {
12645
12871
  const toId = resolveRecipient(ctx.store, input.to);
@@ -12736,8 +12962,7 @@ function deleteMessage(ctx, meId, msgId) {
12736
12962
  function deleteMailbox(store, id) {
12737
12963
  store.removeMailbox(id);
12738
12964
  }
12739
- //#endregion
12740
- //#region src/decommission.ts
12965
+ // src/decommission.ts
12741
12966
  /**
12742
12967
  * Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
12743
12968
  * dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
@@ -12773,7 +12998,7 @@ async function decommission(ctx, input) {
12773
12998
  const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
12774
12999
  const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
12775
13000
  const held = rec.worktree?.lease;
12776
- const onPrimary = worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
13001
+ const onPrimary = isPrimaryCheckout(worktreeRoot, primaryRoot);
12777
13002
  if (onPrimary && input.force) throw new Error(`refusing to close "${input.id}" with --force — its worktree is the primary checkout, which close never alters; close it without --force to end its session and reap its record`);
12778
13003
  const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
12779
13004
  if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
@@ -12839,10 +13064,9 @@ function isDirty(exec, worktreeRoot) {
12839
13064
  "--porcelain",
12840
13065
  "--untracked-files=all"
12841
13066
  ]);
12842
- return !!status && status.split("\n").some((line) => line && line !== `?? ${WORKTREE_MARKER}`);
13067
+ return !!status && status.split("\n").some((line) => line && line !== `?? .agents/cyberlegion/config.json`);
12843
13068
  }
12844
- //#endregion
12845
- //#region src/install.ts
13069
+ // src/install.ts
12846
13070
  const PIN_TOKEN = /^[0-9A-Za-z][0-9A-Za-z._+-]*$/;
12847
13071
  function validatePin(pin) {
12848
13072
  if (!PIN_TOKEN.test(pin)) throw new Error(`invalid --pin "${pin}" — expected a version or dist-tag token like 0.2.0 or latest (no spaces, ranges, or shell metacharacters)`);
@@ -12995,8 +13219,43 @@ function removeCursor(settings, event, canonical) {
12995
13219
  else delete hooks[event];
12996
13220
  return true;
12997
13221
  }
12998
- //#endregion
12999
- //#region src/output.ts
13222
+ // src/landed.ts
13223
+ /**
13224
+ * Which worktrees a unit shares hold work that has landed, as the `landed` judgment `prune` takes.
13225
+ * It is the worktree library's own judgment, the one its `acquire` recycles a worktree by: the
13226
+ * checkout is clean and its branch has landed on the default branch. Asked once per repository
13227
+ * before prune's synchronous pass, for every shared worktree still on disk. A worktree git cannot
13228
+ * judge (detached, no default branch, a repository git does not know) does not count as landed, so
13229
+ * prune keeps the record that points at it.
13230
+ */
13231
+ async function landedWorktrees(ctx) {
13232
+ const exec = asyncExec(ctx.exec ?? realExec);
13233
+ const roots = /* @__PURE__ */ new Map();
13234
+ for (const rec of listAgents(ctx.store)) {
13235
+ const shared = rec.sharedWorktree;
13236
+ if (shared?.root && existsSync(shared.root)) roots.set(shared.root, shared.primaryRoot);
13237
+ }
13238
+ const landed = /* @__PURE__ */ new Set();
13239
+ const asked = /* @__PURE__ */ new Set();
13240
+ for (const [root, recorded] of roots) {
13241
+ const primary = await primaryRoot({
13242
+ from: root,
13243
+ exec
13244
+ }).catch(() => recorded);
13245
+ if (!primary || asked.has(primary)) continue;
13246
+ asked.add(primary);
13247
+ const entries = await listWorktrees({
13248
+ primaryRoot: primary,
13249
+ exec,
13250
+ ignore: LEASE_IGNORE
13251
+ }).catch(() => []);
13252
+ for (const e of entries) if (e.linked && e.merged === true && e.dirty === false) landed.add(e.root);
13253
+ }
13254
+ const normalized = /* @__PURE__ */ new Map();
13255
+ for (const root of roots.keys()) normalized.set(root, landed.has(await normalizeWorktreePath(root)));
13256
+ return (root) => normalized.get(root) ?? false;
13257
+ }
13258
+ // src/output.ts
13000
13259
  function stringifyCell(v) {
13001
13260
  if (v == null) return "";
13002
13261
  const s = String(v);
@@ -13029,8 +13288,7 @@ function fail(msg) {
13029
13288
  console.error(JSON.stringify({ error: msg }));
13030
13289
  process.exit(1);
13031
13290
  }
13032
- //#endregion
13033
- //#region src/participants.ts
13291
+ // src/participants.ts
13034
13292
  const UNIT_PREFIX = "cyberlegion:unit/";
13035
13293
  /** This hub's own participant key. One per hub root, so reconciling one hub (a `--space`, say)
13036
13294
  * never retires the units another hub registered in the same cynapse store. */
@@ -13194,8 +13452,7 @@ async function syncCynapse(hub, env = process.env, load = importCynapse) {
13194
13452
  cyn?.close();
13195
13453
  }
13196
13454
  }
13197
- //#endregion
13198
- //#region src/permission.ts
13455
+ // src/permission.ts
13199
13456
  const CLI_RULE = "Bash(cyberlegion *)";
13200
13457
  const COVERING = /^Bash(?:\((?:\*|cyberlegion(?: \*|:\*|\*))\))?$/;
13201
13458
  /** Claude Code's user settings file: `$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`. */
@@ -13246,8 +13503,7 @@ function addPermissionRule(file) {
13246
13503
  writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
13247
13504
  return "added";
13248
13505
  }
13249
- //#endregion
13250
- //#region src/presence-spawn.ts
13506
+ // src/presence-spawn.ts
13251
13507
  /** The brief a presence spawned into a standing owner's home starts from: whom it stands in for, and
13252
13508
  * where that owner's mail is. What to *do* with the mail is the home's agent definition's business. */
13253
13509
  function presenceBrief(handle) {
@@ -13342,8 +13598,7 @@ async function spawnPresence(ctx, owner, options = {}) {
13342
13598
  ...turn
13343
13599
  };
13344
13600
  }
13345
- //#endregion
13346
- //#region src/project.ts
13601
+ // src/project.ts
13347
13602
  /**
13348
13603
  * The canonical git common dir for `dir`, or undefined outside a repository. The common dir is the
13349
13604
  * one path every checkout of a repository shares — the default checkout and each linked worktree
@@ -13462,8 +13717,7 @@ function byPath(ctx, dir) {
13462
13717
  if (!commonDir) throw new Error(`"${dir}" is not inside a git repository`);
13463
13718
  return ctx.store.getProject(projectIdOf(commonDir)) ?? registerProject(ctx, { dir });
13464
13719
  }
13465
- //#endregion
13466
- //#region src/runtime/inject-inbox.ts
13720
+ // src/runtime/inject-inbox.ts
13467
13721
  const EVENTS = ["SessionStart"];
13468
13722
  /**
13469
13723
  * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
@@ -13518,8 +13772,7 @@ function injectInbox(ctx, event) {
13518
13772
  additionalContext: parts.join("\n\n")
13519
13773
  } };
13520
13774
  }
13521
- //#endregion
13522
- //#region src/service.ts
13775
+ // src/service.ts
13523
13776
  /** A refused ownership transition. `stale` means the caller's generation, token, or holder claim is
13524
13777
  * not the current one — the caller is not (or no longer) the authority it claims to be. */
13525
13778
  var ServiceOwnershipError = class extends Error {
@@ -13824,8 +14077,7 @@ async function startService(ctx, projectRef, name, input) {
13824
14077
  throw err;
13825
14078
  }
13826
14079
  }
13827
- //#endregion
13828
- //#region src/standing-home.ts
14080
+ // src/standing-home.ts
13829
14081
  /**
13830
14082
  * Decide what a registration does to a standing owner's home: `undefined` keeps whatever the record
13831
14083
  * already has, `null` drops it, and a `StandingHome` replaces it. Every way a home can be wrong is
@@ -13872,8 +14124,7 @@ function resolveHomeFlags(exec, flags) {
13872
14124
  ...launch
13873
14125
  };
13874
14126
  }
13875
- //#endregion
13876
- //#region src/store/errors.ts
14127
+ // src/store/errors.ts
13877
14128
  /** A record file exists but its content didn't parse as JSON — a torn write (crash mid-`writeFileSync`,
13878
14129
  * pre-atomic-write code path) or on-disk tampering. Carries the file path and the original parse
13879
14130
  * error so a caller can report exactly what's broken and where, rather than "Unexpected token" with
@@ -13886,8 +14137,7 @@ var CorruptRecordError = class extends Error {
13886
14137
  this.name = "CorruptRecordError";
13887
14138
  }
13888
14139
  };
13889
- //#endregion
13890
- //#region src/store/process-liveness.ts
14140
+ // src/store/process-liveness.ts
13891
14141
  /**
13892
14142
  * Probe whether `pid` is alive using `process.kill(pid, 0)` — the POSIX "does this process exist"
13893
14143
  * idiom; it sends no signal, it only tests deliverability. `ESRCH` (no such process) is the only
@@ -13907,8 +14157,7 @@ function probeProcess(pid) {
13907
14157
  return err.code === "ESRCH" ? "dead" : "unknown";
13908
14158
  }
13909
14159
  }
13910
- //#endregion
13911
- //#region src/store/lock.ts
14160
+ // src/store/lock.ts
13912
14161
  var LockTimeoutError = class extends Error {
13913
14162
  name;
13914
14163
  constructor(name) {
@@ -14045,8 +14294,7 @@ async function withLockAsync(root, name, fn, opts) {
14045
14294
  handle.release();
14046
14295
  }
14047
14296
  }
14048
- //#endregion
14049
- //#region src/store/file-store.ts
14297
+ // src/store/file-store.ts
14050
14298
  /** Parse a record file's content, wrapping a `JSON.parse` failure in a typed, file-named
14051
14299
  * `CorruptRecordError` instead of letting a bare `SyntaxError` bubble up from deep inside
14052
14300
  * `listInbox`/`listAgents`/`getAgent` with no indication of WHICH file broke. A file that doesn't
@@ -14256,8 +14504,7 @@ var FileStore = class {
14256
14504
  return withLockAsync(this.root, name, fn);
14257
14505
  }
14258
14506
  };
14259
- //#endregion
14260
- //#region src/unit-runtime.ts
14507
+ // src/unit-runtime.ts
14261
14508
  /** A standing or service record is an address with no runtime of its own — nothing to stop or start. */
14262
14509
  function assertHasRuntime(rec, ref) {
14263
14510
  if (rec.kind === "standing" || rec.kind === "service") throw new Error(`"${ref}" is a ${rec.kind} record — it has no runtime to stop, restart, or rebind`);
@@ -14345,8 +14592,9 @@ function stopUnit(ctx, ref) {
14345
14592
  * record to the new pane, and ring it. When the record carries the harness's conversation id and the
14346
14593
  * harness can resume by id, the session resumes that conversation and is rung to continue, with the
14347
14594
  * plain launch as a shell fallback should the harness reject the id; otherwise (or with `fresh`) it
14348
- * starts empty and is rung to read its brief — a rebrief. Every refusal runs before anything is torn
14349
- * down.
14595
+ * starts empty and is rung to read its brief — a rebrief. An exited unit is refused: its session
14596
+ * ended without a `unit stop`, and the multiplexer that resumes agents has already brought back any
14597
+ * that were coming back. Every refusal runs before anything is torn down.
14350
14598
  *
14351
14599
  * The unit is recorded `stopped` before the open, so a restart interrupted between the two leaves an
14352
14600
  * ordinary stopped unit and a rerun recovers it; nothing needs a separate repair path.
@@ -14355,6 +14603,7 @@ async function restartUnit(ctx, ref, options = {}) {
14355
14603
  const rec = resolveAgent(ctx.store, ref);
14356
14604
  assertHasRuntime(rec, ref);
14357
14605
  assertNotSelf(ctx, rec, "restart");
14606
+ if (rec.status === "exited") throw new Error(`unit "${rec.handle}" has exited — restart takes only a running or stopped unit; spawn a new unit instead, or run unit rebind ${rec.handle} from a session resumed by hand`);
14358
14607
  const harness = rec.harness;
14359
14608
  if (!harness || !(harness in LAUNCH_MAP)) throw new Error(`unit "${rec.handle}" has harness "${rec.harness ?? ""}", not in the launch map (${Object.keys(LAUNCH_MAP).join(" | ")}) — cannot restart it`);
14360
14609
  if (!existsSync(rec.cwd)) throw new Error(`unit "${rec.handle}" cannot restart — its cwd ${rec.cwd} is gone; spawn a new unit instead`);
@@ -14396,10 +14645,7 @@ async function restartUnit(ctx, ref, options = {}) {
14396
14645
  const bound = {
14397
14646
  ...stopped,
14398
14647
  status: "active",
14399
- pane: mux === "tmux" || mux === "herdr" ? {
14400
- mux,
14401
- id: target.id
14402
- } : null,
14648
+ pane: openedPaneLocator(exec, mux, target.id),
14403
14649
  lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
14404
14650
  };
14405
14651
  saveAgent(ctx.store, bound);
@@ -14425,6 +14671,10 @@ function probeLiveness(ctx, rec) {
14425
14671
  if (rec.kind === "standing" || rec.kind === "service") return "none";
14426
14672
  if (rec.status === "stopped") return "stopped";
14427
14673
  if (rec.status === "exited") return "exited";
14674
+ if (rec.pane?.mux === "herdr" && rec.pane.terminal) {
14675
+ const at = locateTerminal(ctx.exec ?? realExec, rec.pane.terminal);
14676
+ return at === void 0 ? "unknown" : at === "gone" ? "gone" : "live";
14677
+ }
14428
14678
  const pane = runtimePane(ctx, rec);
14429
14679
  const adapter = pane ? paneAdapter(ctx, rec) : void 0;
14430
14680
  if (!pane || !adapter) return "unknown";
@@ -14441,7 +14691,7 @@ function probeLiveness(ctx, rec) {
14441
14691
  function rebindUnit(ctx, ref) {
14442
14692
  const rec = resolveAgent(ctx.store, ref);
14443
14693
  assertHasRuntime(rec, ref);
14444
- const cur = storablePane(ctx.env ?? process.env);
14694
+ const cur = callingStorablePane(ctx);
14445
14695
  if (!cur) throw new Error("unit rebind needs to run inside a tmux or herdr pane — there is no pane to bind");
14446
14696
  const oldPane = runtimePane(ctx, rec);
14447
14697
  if (rec.pane?.mux === cur.mux && rec.pane.id === cur.pane && ctx.store.resolvePaneId(cur.pane) === rec.id) return rec;
@@ -14449,13 +14699,15 @@ function rebindUnit(ctx, ref) {
14449
14699
  const holder = holderId && holderId !== rec.id ? loadAgent(ctx.store, holderId) : void 0;
14450
14700
  if (holder && holder.status !== "stopped" && holder.status !== "exited") throw new Error(`this pane belongs to unit "${holder.handle}" (${holder.id}) — rebind never takes another unit's pane`);
14451
14701
  const liveness = probeLiveness(ctx, rec);
14452
- if (oldPane && oldPane !== cur.pane && (liveness === "live" || liveness === "unknown")) throw new Error(`unit "${rec.handle}" may still be running in pane ${oldPane} — stop it first (unit stop ${rec.handle})`);
14702
+ const ownTerminal = !!cur.terminal && rec.pane?.mux === "herdr" && rec.pane.terminal === cur.terminal;
14703
+ if (oldPane && oldPane !== cur.pane && !ownTerminal && (liveness === "live" || liveness === "unknown")) throw new Error(`unit "${rec.handle}" may still be running in pane ${oldPane} — stop it first (unit stop ${rec.handle})`);
14453
14704
  const bound = {
14454
14705
  ...rec,
14455
14706
  status: "active",
14456
14707
  pane: {
14457
14708
  mux: cur.mux,
14458
- id: cur.pane
14709
+ id: cur.pane,
14710
+ ...cur.terminal ? { terminal: cur.terminal } : {}
14459
14711
  },
14460
14712
  lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
14461
14713
  };
@@ -14483,7 +14735,7 @@ function controlsFor(ctx, rec, liveness) {
14483
14735
  if (hasHonestReset(rec.harness)) controls.push("clear");
14484
14736
  }
14485
14737
  if (runtimePane(ctx, rec) !== void 0 && (liveness === "live" || liveness === "gone" || liveness === "unknown")) controls.push("stop");
14486
- if (rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
14738
+ if (liveness !== "exited" && rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
14487
14739
  if (liveness === "stopped" || liveness === "exited" || liveness === "gone") controls.push("rebind");
14488
14740
  controls.push("close");
14489
14741
  return controls;
@@ -14538,8 +14790,7 @@ function recordConversation(ctx, hookInput) {
14538
14790
  conversation: id
14539
14791
  });
14540
14792
  }
14541
- //#endregion
14542
- //#region src/wake/await.ts
14793
+ // src/wake/await.ts
14543
14794
  /** Internal poll interval — not user-facing (the round-trip cost of one `mail await` cycle). */
14544
14795
  const POLL_MS$1 = 1e3;
14545
14796
  const DEFAULT_TIMEOUT_MS = 6e5;
@@ -14584,8 +14835,7 @@ async function awaitReply(ctx, input) {
14584
14835
  await sleep(POLL_MS$1);
14585
14836
  }
14586
14837
  }
14587
- //#endregion
14588
- //#region src/wake/watch.ts
14838
+ // src/wake/watch.ts
14589
14839
  const POLL_MS = 1e3;
14590
14840
  /**
14591
14841
  * A continuous foreground observer: prints each NEW matching message as it arrives, polling on a
@@ -14617,8 +14867,7 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
14617
14867
  await sleep(POLL_MS);
14618
14868
  }
14619
14869
  }
14620
- //#endregion
14621
- //#region src/cli.ts
14870
+ // src/cli.ts
14622
14871
  const VERSION = globalThis.__SKILL_SCRIPT__?.version ?? JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
14623
14872
  function ctxOf(opts) {
14624
14873
  return {
@@ -14821,18 +15070,25 @@ function runWho(opts) {
14821
15070
  if (agents.length === 0) nextStep("cyberlegion unit register to join");
14822
15071
  }
14823
15072
  syncsUnits(withGlobals(unit.command("who"))).description("list the addressable units").option("--all", "include exited units").option("--reconcile", "live-probe the current mux: cull dead-pane records and adopt unbound harness-bearing panes before listing").action(runWho);
14824
- syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and sweep").action((opts) => {
15073
+ syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and delete exited units that leave no work on disk").action(async (opts) => {
14825
15074
  const ctx = ctxOf(opts);
14826
15075
  touch(ctx);
14827
- const changed = prune(ctx);
15076
+ const changed = prune(ctx, { landed: await landedWorktrees(ctx) });
14828
15077
  emit(formatOf(opts), {
14829
- toon: toonList("pruned", changed, [{
14830
- key: "id",
14831
- get: (a) => a.id
14832
- }, {
14833
- key: "handle",
14834
- get: (a) => a.handle
14835
- }], `${changed.length} pruned`),
15078
+ toon: toonList("pruned", changed, [
15079
+ {
15080
+ key: "id",
15081
+ get: (a) => a.id
15082
+ },
15083
+ {
15084
+ key: "handle",
15085
+ get: (a) => a.handle
15086
+ },
15087
+ {
15088
+ key: "pruned",
15089
+ get: (a) => a.pruned
15090
+ }
15091
+ ], `${changed.length} pruned (${changed.filter((a) => a.pruned === "deleted").length} deleted)`),
14836
15092
  json: changed
14837
15093
  });
14838
15094
  });
@@ -15819,5 +16075,4 @@ async function runCli(argv = process.argv) {
15819
16075
  process.exit(1);
15820
16076
  }
15821
16077
  }
15822
- //#endregion
15823
16078
  export { runCli };