cyberlegion 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,14 +10647,14 @@ 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),
10518
10655
  harness,
10519
10656
  cwd: process.cwd(),
10520
- worktree: existing?.worktree ? keptWorktree(existing.worktree, exec) : gitWorktree(exec),
10657
+ ...checkoutFor(ctx.store, exec, id, existing),
10521
10658
  pane: cur ? paneLocator(cur, exec, env) : null,
10522
10659
  status: "active",
10523
10660
  createdAt: existing?.createdAt ?? ts,
@@ -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
@@ -10584,24 +10722,71 @@ function writeStanding(ctx, id, input) {
10584
10722
  saveAgent(ctx.store, rec);
10585
10723
  return rec;
10586
10724
  }
10587
- /** The checkout the cwd sits in: its root, its branch, and the repository's primary checkout — the
10588
- * same `primaryRoot` spawn records, so a lookup by repository finds a unit in a linked worktree by
10589
- * the repository's name rather than the worktree folder's. */
10590
- function gitWorktree(exec) {
10591
- const root = exec("git", ["rev-parse", "--show-toplevel"]);
10725
+ /** The checkout `dir` (else the cwd) sits in: its root, its branch, and the repository's primary
10726
+ * checkout — the same `primaryRoot` spawn records, so a lookup by repository finds a unit in a linked
10727
+ * worktree by the repository's name rather than the worktree folder's. */
10728
+ function gitWorktree(exec, dir) {
10729
+ const git = dir === void 0 ? exec : (cmd, args) => exec(cmd, cmd === "git" ? [
10730
+ "-C",
10731
+ dir,
10732
+ ...args
10733
+ ] : args);
10734
+ const root = git("git", ["rev-parse", "--show-toplevel"]);
10592
10735
  if (!root) return null;
10593
- const branch = exec("git", [
10736
+ const branch = git("git", [
10594
10737
  "rev-parse",
10595
10738
  "--abbrev-ref",
10596
10739
  "HEAD"
10597
10740
  ]) ?? void 0;
10598
- const primaryRoot = primaryRootHere(exec);
10741
+ const primaryRoot = primaryRootHere(git);
10599
10742
  return {
10600
10743
  root,
10601
10744
  ...branch !== void 0 ? { branch } : {},
10602
10745
  ...primaryRoot !== void 0 ? { primaryRoot } : {}
10603
10746
  };
10604
10747
  }
10748
+ /** The id of the unit, other than `self`, whose record owns the worktree at `root`. */
10749
+ function ownerOf(store, root, self) {
10750
+ const at = resolve(root);
10751
+ return listAgents(store).find((a) => a.id !== self && a.worktree?.root && resolve(a.worktree.root) === at)?.id;
10752
+ }
10753
+ /** `checkout` as a shared reference, naming the unit that owns it when one does. */
10754
+ function asShared(store, checkout, self) {
10755
+ const owner = ownerOf(store, checkout.root, self);
10756
+ return {
10757
+ ...checkout,
10758
+ ...owner ? { owner } : {}
10759
+ };
10760
+ }
10761
+ /** The checkout a `--cwd` spawn opens into, as a shared reference — spawn created none, so the unit
10762
+ * owns none, whoever does. `undefined` when `dir` is in no git checkout. */
10763
+ function sharedCheckoutAt(store, exec, dir) {
10764
+ const checkout = gitWorktree(exec, dir);
10765
+ return checkout ? asShared(store, checkout) : void 0;
10766
+ }
10767
+ /**
10768
+ * Which checkout a registering session owns and which it only shares. An existing owned worktree is
10769
+ * kept. Otherwise the cwd's checkout is the session's own — unless another unit's record owns it, or
10770
+ * this unit already records it as shared (a `--cwd` spawn): then it is a shared reference, and the
10771
+ * session owns none, so its close can never remove another unit's worktree or release its lease.
10772
+ */
10773
+ function checkoutFor(store, exec, id, existing) {
10774
+ const kept = existing?.sharedWorktree ? { sharedWorktree: existing.sharedWorktree } : {};
10775
+ if (existing?.worktree) return {
10776
+ worktree: keptWorktree(existing.worktree, exec),
10777
+ ...kept
10778
+ };
10779
+ const here = gitWorktree(exec);
10780
+ if (!here) return {
10781
+ worktree: null,
10782
+ ...kept
10783
+ };
10784
+ if (existing?.sharedWorktree || ownerOf(store, here.root, id)) return {
10785
+ worktree: null,
10786
+ sharedWorktree: asShared(store, here, id)
10787
+ };
10788
+ return { worktree: here };
10789
+ }
10605
10790
  function primaryRootHere(exec) {
10606
10791
  try {
10607
10792
  return resolvePrimaryRoot(exec);
@@ -10808,10 +10993,26 @@ function bumpLastSeen(ctx, id) {
10808
10993
  rec.lastSeen = nowIso(ctx);
10809
10994
  saveAgent(ctx.store, rec);
10810
10995
  }
10811
- /** 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
+ */
10812
11001
  function touch(ctx) {
10813
- const id = resolveSelfId(ctx);
10814
- 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);
10815
11016
  }
10816
11017
  const STALE_MS = 9e5;
10817
11018
  /** Standing and service records have no session of their own, so nothing about a pane or a timer
@@ -10838,7 +11039,9 @@ function isPrunable(rec) {
10838
11039
  function sessionLive(ctx, rec) {
10839
11040
  if (rec.status === "exited" || rec.status === "stopped") return false;
10840
11041
  if (!rec.pane) return true;
10841
- 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);
10842
11045
  if (panes.length === 0) return true;
10843
11046
  const id = rec.pane.id;
10844
11047
  return panes.some((p) => p.id === id);
@@ -10913,11 +11116,12 @@ function reconcile(ctx, opts) {
10913
11116
  if (panes.length === 0) return [];
10914
11117
  const live = new Set(panes.map((p) => p.id));
10915
11118
  const changed = [];
11119
+ const cache = {};
10916
11120
  for (const rec of listAgents(ctx.store)) {
10917
11121
  if (!isPrunable(rec)) continue;
10918
11122
  if (!rec.pane) continue;
10919
11123
  if (rec.pane.mux !== cur.mux) continue;
10920
- 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)) {
10921
11125
  rec.status = "exited";
10922
11126
  saveAgent(ctx.store, rec);
10923
11127
  changed.push(rec);
@@ -10942,27 +11146,93 @@ function paneGone(exec, pane, lists) {
10942
11146
  }
10943
11147
  return panes.length > 0 && !panes.some((p) => p.id === pane.id);
10944
11148
  }
10945
- /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
10946
- * first, then judges each remaining record (covers the other mux and sessions outside any
10947
- * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
10948
- * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
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 worktree is still on disk is kept, since
11167
+ * `unit close` owns removing that worktree; every other exited unit is deleted with `deleteExited`.
11168
+ */
10949
11169
  function prune(ctx) {
10950
11170
  const exec = ctx.exec ?? realExec;
10951
11171
  const now = ctx.now?.() ?? Date.now();
10952
- const changed = reconcile(ctx);
11172
+ const marked = reconcile(ctx);
10953
11173
  const lists = /* @__PURE__ */ new Map();
11174
+ const cache = {};
10954
11175
  for (const rec of listAgents(ctx.store)) {
10955
11176
  if (!isPrunable(rec)) continue;
10956
- if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
11177
+ 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)) {
10957
11178
  rec.status = "exited";
10958
11179
  saveAgent(ctx.store, rec);
10959
- changed.push(rec);
10960
- }
11180
+ marked.push(rec);
11181
+ }
11182
+ }
11183
+ const deleted = deleteExited(ctx);
11184
+ const gone = new Set(deleted.map((r) => r.id));
11185
+ return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
11186
+ ...r,
11187
+ pruned: "exited"
11188
+ })), ...deleted.map((r) => ({
11189
+ ...r,
11190
+ pruned: "deleted"
11191
+ }))];
11192
+ }
11193
+ /** Whether an exited record still has a worktree on disk — the one thing it leaves behind that
11194
+ * `unit close` must deal with. A record that never had a worktree has nothing on disk. */
11195
+ function worktreeOnDisk(rec) {
11196
+ if (rec.worktree?.root == null || !existsSync(rec.worktree.root)) return false;
11197
+ return !isPrimaryCheckout(rec.worktree.root, rec.worktree.primaryRoot);
11198
+ }
11199
+ /** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
11200
+ * `unit prune` share for "a seat on the main checkout, which nothing removes". */
11201
+ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
11202
+ return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
11203
+ }
11204
+ /**
11205
+ * Delete every exited unit with no worktree left to close (none on disk, or only the primary checkout): its record, brief, mailbox, and the pane
11206
+ * pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
11207
+ * stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
11208
+ * resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
11209
+ * since. A worktree that is gone from disk may still be registered with git, and the record is the
11210
+ * last place that names its repository, so that registration is cleared first, best-effort, as
11211
+ * `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
11212
+ * someone else.
11213
+ */
11214
+ function deleteExited(ctx) {
11215
+ const exec = ctx.exec ?? realExec;
11216
+ const deleted = [];
11217
+ for (const rec of listAgents(ctx.store)) {
11218
+ if (rec.status !== "exited" || isSessionIndependent(rec) || worktreeOnDisk(rec)) continue;
11219
+ const primaryRoot = rec.worktree?.primaryRoot;
11220
+ if (primaryRoot && existsSync(primaryRoot)) exec("git", [
11221
+ "-C",
11222
+ primaryRoot,
11223
+ "worktree",
11224
+ "prune"
11225
+ ]);
11226
+ const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(rec.id);
11227
+ if (pane && ctx.store.resolvePaneId(pane) === rec.id) ctx.store.removePaneIndex(pane);
11228
+ ctx.store.removeAgent(rec.id);
11229
+ ctx.store.removeAgentData(rec.id);
11230
+ ctx.store.removeMailbox(rec.id);
11231
+ deleted.push(rec);
10961
11232
  }
10962
- return changed;
11233
+ return deleted;
10963
11234
  }
10964
- //#endregion
10965
- //#region src/console/prompt-guard.ts
11235
+ // src/console/prompt-guard.ts
10966
11236
  const CODEX_PLACEHOLDERS = /* @__PURE__ */ new Set([
10967
11237
  "Ask Codex to do anything",
10968
11238
  "Explain this codebase",
@@ -11120,8 +11390,7 @@ async function clearLine(adapter, exec, target, sleep) {
11120
11390
  adapter.sendText(exec, target, KILL_LINE);
11121
11391
  await sleep(KEY_SETTLE_MS);
11122
11392
  }
11123
- //#endregion
11124
- //#region src/console/ring.ts
11393
+ // src/console/ring.ts
11125
11394
  const DEFAULT_ATTEMPTS = 10;
11126
11395
  const DEFAULT_SETTLE_MS = 400;
11127
11396
  /** cyber-mux's `isStaged` matches on the same prefix length. */
@@ -11226,8 +11495,7 @@ async function ringTurn(adapter, exec, target, message, opts = {}) {
11226
11495
  const why = state === "lost" ? "the text vanished without the harness posting it" : `input still staged after ${attempts} re-submit attempts`;
11227
11496
  throw new Error(`nudge failed: peer at pane ${target.id} never took the turn — ${why}`);
11228
11497
  }
11229
- //#endregion
11230
- //#region src/console/doorbell.ts
11498
+ // src/console/doorbell.ts
11231
11499
  /** The doorbell text delivered to a woken recipient; also the standalone `unit nudge` default. */
11232
11500
  const DELIVERY_DOORBELL = "You have unread mail — check your inbox.";
11233
11501
  /**
@@ -11426,8 +11694,7 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
11426
11694
  };
11427
11695
  }
11428
11696
  }
11429
- //#endregion
11430
- //#region src/console/trust.ts
11697
+ // src/console/trust.ts
11431
11698
  /**
11432
11699
  * Each harness's folder-trust prompt, as it draws it, and the keys that accept it. Read from Claude
11433
11700
  * Code 2.1.286, Codex 0.153.4 (and its newer "Trust this folder?" wording), and cursor-agent
@@ -11537,8 +11804,7 @@ function stuck(where) {
11537
11804
  message: `${where}, and the prompt is still showing after spawn sent the keys that accept it. A person must answer it.`
11538
11805
  };
11539
11806
  }
11540
- //#endregion
11541
- //#region src/mux-select.ts
11807
+ // src/mux-select.ts
11542
11808
  /**
11543
11809
  * Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
11544
11810
  * `$CYBERLEGION_MUX*` → `$CYBER_MUX*` env seam (`mux-env.ts`) — tmux/herdr map to their existing
@@ -11558,8 +11824,7 @@ function selectSessionAdapter(env, exec = realExec) {
11558
11824
  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`);
11559
11825
  throw new Error("spawn requires a session backend — run inside tmux ($TMUX) or herdr ($HERDR_ENV=1)");
11560
11826
  }
11561
- //#endregion
11562
- //#region src/workspace-label.ts
11827
+ // src/workspace-label.ts
11563
11828
  /** Leading actions that tear down or revert — the `A2-` class, checked first. */
11564
11829
  const TEARDOWN_ACTIONS = /* @__PURE__ */ new Set([
11565
11830
  "clean",
@@ -11702,8 +11967,7 @@ function fitWords(words, budget) {
11702
11967
  }
11703
11968
  return out;
11704
11969
  }
11705
- //#endregion
11706
- //#region src/worktree-lease.ts
11970
+ // src/worktree-lease.ts
11707
11971
  /**
11708
11972
  * The two worktree-library calls a unit's lifetime makes: `unit spawn` acquires a worktree, `unit
11709
11973
  * close` releases it. Gathered here so a test can stand in for them and drive spawn's routing without
@@ -11727,8 +11991,7 @@ function leaseHolder(id) {
11727
11991
  * cyberlegion's own file, not the unit's work, and counting it would keep every slot from reuse in a
11728
11992
  * project that does not track it. */
11729
11993
  const LEASE_IGNORE = [WORKTREE_MARKER];
11730
- //#endregion
11731
- //#region src/session.ts
11994
+ // src/session.ts
11732
11995
  /** How each harness's own CLI is launched in the new pane. */
11733
11996
  const LAUNCH_MAP = {
11734
11997
  claude: "claude",
@@ -11852,6 +12115,7 @@ async function spawn(ctx, input) {
11852
12115
  const from = callerPane(sessionAdapter, normalizedEnv);
11853
12116
  let cwd;
11854
12117
  let worktree;
12118
+ let sharedWorktree;
11855
12119
  let base;
11856
12120
  let reused;
11857
12121
  let target;
@@ -11860,6 +12124,7 @@ async function spawn(ctx, input) {
11860
12124
  cwd = resolve(input.cwd);
11861
12125
  if (primaryRoot$1 !== void 0) assertDistinctFromPrimary(cwd, primaryRoot$1);
11862
12126
  worktree = null;
12127
+ sharedWorktree = sharedCheckoutAt(ctx.store, exec, cwd);
11863
12128
  const at = input.at ?? "tab";
11864
12129
  target = sessionAdapter.open(exec, {
11865
12130
  cwd,
@@ -11950,10 +12215,8 @@ async function spawn(ctx, input) {
11950
12215
  harness,
11951
12216
  cwd,
11952
12217
  worktree,
11953
- pane: muxName === "tmux" || muxName === "herdr" ? {
11954
- mux: muxName,
11955
- id: target.id
11956
- } : null,
12218
+ ...sharedWorktree ? { sharedWorktree } : {},
12219
+ pane: openedPaneLocator(exec, muxName, target.id),
11957
12220
  status: "active",
11958
12221
  createdAt: ts,
11959
12222
  lastSeen: ts,
@@ -12275,8 +12538,7 @@ function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
12275
12538
  if (input.task != null && input.task !== "") return input.task;
12276
12539
  return null;
12277
12540
  }
12278
- //#endregion
12279
- //#region src/agentdef/resolve.ts
12541
+ // src/agentdef/resolve.ts
12280
12542
  function parseAgentDefFile(text) {
12281
12543
  const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
12282
12544
  if (!m) return {
@@ -12396,8 +12658,7 @@ function listAgentDefs(input = {}) {
12396
12658
  }
12397
12659
  return defs;
12398
12660
  }
12399
- //#endregion
12400
- //#region src/agentdef/realize.ts
12661
+ // src/agentdef/realize.ts
12401
12662
  const DEFAULT_HARNESS = "claude";
12402
12663
  /** POSIX single-quote a value for safe inclusion in a shell command line. */
12403
12664
  function shellQuote(value) {
@@ -12515,8 +12776,7 @@ function resolveSpawnLaunch(input) {
12515
12776
  ...input.searchRoots ? { searchRoots: input.searchRoots } : {}
12516
12777
  }), overrides);
12517
12778
  }
12518
- //#endregion
12519
- //#region src/cli-input.ts
12779
+ // src/cli-input.ts
12520
12780
  /**
12521
12781
  * Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
12522
12782
  * `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
@@ -12564,8 +12824,7 @@ function spawnCommandInput(opts, listCursorModels) {
12564
12824
  function readCommandOutput(format, result) {
12565
12825
  return format === "json" ? JSON.stringify(result, null, 2) : result.output;
12566
12826
  }
12567
- //#endregion
12568
- //#region src/cursor-models.ts
12827
+ // src/cursor-models.ts
12569
12828
  /** The model ids in `cursor-agent models` output: the first token of each line, ANSI colour and list
12570
12829
  * bullets stripped. Headings and tips come along too; callers only test a specific id for membership. */
12571
12830
  function parseCursorModels(output) {
@@ -12588,8 +12847,7 @@ function listCursorModels() {
12588
12847
  return [];
12589
12848
  }
12590
12849
  }
12591
- //#endregion
12592
- //#region src/message.ts
12850
+ // src/message.ts
12593
12851
  /** Write one message into the recipient's inbox. Collision-free by <epochMs>-<hex>. */
12594
12852
  function send(ctx, input) {
12595
12853
  const toId = resolveRecipient(ctx.store, input.to);
@@ -12686,8 +12944,7 @@ function deleteMessage(ctx, meId, msgId) {
12686
12944
  function deleteMailbox(store, id) {
12687
12945
  store.removeMailbox(id);
12688
12946
  }
12689
- //#endregion
12690
- //#region src/decommission.ts
12947
+ // src/decommission.ts
12691
12948
  /**
12692
12949
  * Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
12693
12950
  * dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
@@ -12723,7 +12980,7 @@ async function decommission(ctx, input) {
12723
12980
  const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
12724
12981
  const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
12725
12982
  const held = rec.worktree?.lease;
12726
- const onPrimary = worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
12983
+ const onPrimary = isPrimaryCheckout(worktreeRoot, primaryRoot);
12727
12984
  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`);
12728
12985
  const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
12729
12986
  if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
@@ -12789,10 +13046,9 @@ function isDirty(exec, worktreeRoot) {
12789
13046
  "--porcelain",
12790
13047
  "--untracked-files=all"
12791
13048
  ]);
12792
- return !!status && status.split("\n").some((line) => line && line !== `?? ${WORKTREE_MARKER}`);
13049
+ return !!status && status.split("\n").some((line) => line && line !== `?? .agents/cyberlegion/config.json`);
12793
13050
  }
12794
- //#endregion
12795
- //#region src/install.ts
13051
+ // src/install.ts
12796
13052
  const PIN_TOKEN = /^[0-9A-Za-z][0-9A-Za-z._+-]*$/;
12797
13053
  function validatePin(pin) {
12798
13054
  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)`);
@@ -12945,8 +13201,7 @@ function removeCursor(settings, event, canonical) {
12945
13201
  else delete hooks[event];
12946
13202
  return true;
12947
13203
  }
12948
- //#endregion
12949
- //#region src/output.ts
13204
+ // src/output.ts
12950
13205
  function stringifyCell(v) {
12951
13206
  if (v == null) return "";
12952
13207
  const s = String(v);
@@ -12979,8 +13234,7 @@ function fail(msg) {
12979
13234
  console.error(JSON.stringify({ error: msg }));
12980
13235
  process.exit(1);
12981
13236
  }
12982
- //#endregion
12983
- //#region src/participants.ts
13237
+ // src/participants.ts
12984
13238
  const UNIT_PREFIX = "cyberlegion:unit/";
12985
13239
  /** This hub's own participant key. One per hub root, so reconciling one hub (a `--space`, say)
12986
13240
  * never retires the units another hub registered in the same cynapse store. */
@@ -13144,8 +13398,7 @@ async function syncCynapse(hub, env = process.env, load = importCynapse) {
13144
13398
  cyn?.close();
13145
13399
  }
13146
13400
  }
13147
- //#endregion
13148
- //#region src/permission.ts
13401
+ // src/permission.ts
13149
13402
  const CLI_RULE = "Bash(cyberlegion *)";
13150
13403
  const COVERING = /^Bash(?:\((?:\*|cyberlegion(?: \*|:\*|\*))\))?$/;
13151
13404
  /** Claude Code's user settings file: `$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`. */
@@ -13196,8 +13449,7 @@ function addPermissionRule(file) {
13196
13449
  writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
13197
13450
  return "added";
13198
13451
  }
13199
- //#endregion
13200
- //#region src/presence-spawn.ts
13452
+ // src/presence-spawn.ts
13201
13453
  /** The brief a presence spawned into a standing owner's home starts from: whom it stands in for, and
13202
13454
  * where that owner's mail is. What to *do* with the mail is the home's agent definition's business. */
13203
13455
  function presenceBrief(handle) {
@@ -13292,8 +13544,7 @@ async function spawnPresence(ctx, owner, options = {}) {
13292
13544
  ...turn
13293
13545
  };
13294
13546
  }
13295
- //#endregion
13296
- //#region src/project.ts
13547
+ // src/project.ts
13297
13548
  /**
13298
13549
  * The canonical git common dir for `dir`, or undefined outside a repository. The common dir is the
13299
13550
  * one path every checkout of a repository shares — the default checkout and each linked worktree
@@ -13412,8 +13663,7 @@ function byPath(ctx, dir) {
13412
13663
  if (!commonDir) throw new Error(`"${dir}" is not inside a git repository`);
13413
13664
  return ctx.store.getProject(projectIdOf(commonDir)) ?? registerProject(ctx, { dir });
13414
13665
  }
13415
- //#endregion
13416
- //#region src/runtime/inject-inbox.ts
13666
+ // src/runtime/inject-inbox.ts
13417
13667
  const EVENTS = ["SessionStart"];
13418
13668
  /**
13419
13669
  * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
@@ -13468,8 +13718,7 @@ function injectInbox(ctx, event) {
13468
13718
  additionalContext: parts.join("\n\n")
13469
13719
  } };
13470
13720
  }
13471
- //#endregion
13472
- //#region src/service.ts
13721
+ // src/service.ts
13473
13722
  /** A refused ownership transition. `stale` means the caller's generation, token, or holder claim is
13474
13723
  * not the current one — the caller is not (or no longer) the authority it claims to be. */
13475
13724
  var ServiceOwnershipError = class extends Error {
@@ -13774,8 +14023,7 @@ async function startService(ctx, projectRef, name, input) {
13774
14023
  throw err;
13775
14024
  }
13776
14025
  }
13777
- //#endregion
13778
- //#region src/standing-home.ts
14026
+ // src/standing-home.ts
13779
14027
  /**
13780
14028
  * Decide what a registration does to a standing owner's home: `undefined` keeps whatever the record
13781
14029
  * already has, `null` drops it, and a `StandingHome` replaces it. Every way a home can be wrong is
@@ -13822,8 +14070,7 @@ function resolveHomeFlags(exec, flags) {
13822
14070
  ...launch
13823
14071
  };
13824
14072
  }
13825
- //#endregion
13826
- //#region src/store/errors.ts
14073
+ // src/store/errors.ts
13827
14074
  /** A record file exists but its content didn't parse as JSON — a torn write (crash mid-`writeFileSync`,
13828
14075
  * pre-atomic-write code path) or on-disk tampering. Carries the file path and the original parse
13829
14076
  * error so a caller can report exactly what's broken and where, rather than "Unexpected token" with
@@ -13836,8 +14083,7 @@ var CorruptRecordError = class extends Error {
13836
14083
  this.name = "CorruptRecordError";
13837
14084
  }
13838
14085
  };
13839
- //#endregion
13840
- //#region src/store/process-liveness.ts
14086
+ // src/store/process-liveness.ts
13841
14087
  /**
13842
14088
  * Probe whether `pid` is alive using `process.kill(pid, 0)` — the POSIX "does this process exist"
13843
14089
  * idiom; it sends no signal, it only tests deliverability. `ESRCH` (no such process) is the only
@@ -13857,8 +14103,7 @@ function probeProcess(pid) {
13857
14103
  return err.code === "ESRCH" ? "dead" : "unknown";
13858
14104
  }
13859
14105
  }
13860
- //#endregion
13861
- //#region src/store/lock.ts
14106
+ // src/store/lock.ts
13862
14107
  var LockTimeoutError = class extends Error {
13863
14108
  name;
13864
14109
  constructor(name) {
@@ -13995,8 +14240,7 @@ async function withLockAsync(root, name, fn, opts) {
13995
14240
  handle.release();
13996
14241
  }
13997
14242
  }
13998
- //#endregion
13999
- //#region src/store/file-store.ts
14243
+ // src/store/file-store.ts
14000
14244
  /** Parse a record file's content, wrapping a `JSON.parse` failure in a typed, file-named
14001
14245
  * `CorruptRecordError` instead of letting a bare `SyntaxError` bubble up from deep inside
14002
14246
  * `listInbox`/`listAgents`/`getAgent` with no indication of WHICH file broke. A file that doesn't
@@ -14206,8 +14450,7 @@ var FileStore = class {
14206
14450
  return withLockAsync(this.root, name, fn);
14207
14451
  }
14208
14452
  };
14209
- //#endregion
14210
- //#region src/unit-runtime.ts
14453
+ // src/unit-runtime.ts
14211
14454
  /** A standing or service record is an address with no runtime of its own — nothing to stop or start. */
14212
14455
  function assertHasRuntime(rec, ref) {
14213
14456
  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`);
@@ -14295,8 +14538,9 @@ function stopUnit(ctx, ref) {
14295
14538
  * record to the new pane, and ring it. When the record carries the harness's conversation id and the
14296
14539
  * harness can resume by id, the session resumes that conversation and is rung to continue, with the
14297
14540
  * plain launch as a shell fallback should the harness reject the id; otherwise (or with `fresh`) it
14298
- * starts empty and is rung to read its brief — a rebrief. Every refusal runs before anything is torn
14299
- * down.
14541
+ * starts empty and is rung to read its brief — a rebrief. An exited unit is refused: its session
14542
+ * ended without a `unit stop`, and the multiplexer that resumes agents has already brought back any
14543
+ * that were coming back. Every refusal runs before anything is torn down.
14300
14544
  *
14301
14545
  * The unit is recorded `stopped` before the open, so a restart interrupted between the two leaves an
14302
14546
  * ordinary stopped unit and a rerun recovers it; nothing needs a separate repair path.
@@ -14305,6 +14549,7 @@ async function restartUnit(ctx, ref, options = {}) {
14305
14549
  const rec = resolveAgent(ctx.store, ref);
14306
14550
  assertHasRuntime(rec, ref);
14307
14551
  assertNotSelf(ctx, rec, "restart");
14552
+ 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`);
14308
14553
  const harness = rec.harness;
14309
14554
  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`);
14310
14555
  if (!existsSync(rec.cwd)) throw new Error(`unit "${rec.handle}" cannot restart — its cwd ${rec.cwd} is gone; spawn a new unit instead`);
@@ -14346,10 +14591,7 @@ async function restartUnit(ctx, ref, options = {}) {
14346
14591
  const bound = {
14347
14592
  ...stopped,
14348
14593
  status: "active",
14349
- pane: mux === "tmux" || mux === "herdr" ? {
14350
- mux,
14351
- id: target.id
14352
- } : null,
14594
+ pane: openedPaneLocator(exec, mux, target.id),
14353
14595
  lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
14354
14596
  };
14355
14597
  saveAgent(ctx.store, bound);
@@ -14375,6 +14617,10 @@ function probeLiveness(ctx, rec) {
14375
14617
  if (rec.kind === "standing" || rec.kind === "service") return "none";
14376
14618
  if (rec.status === "stopped") return "stopped";
14377
14619
  if (rec.status === "exited") return "exited";
14620
+ if (rec.pane?.mux === "herdr" && rec.pane.terminal) {
14621
+ const at = locateTerminal(ctx.exec ?? realExec, rec.pane.terminal);
14622
+ return at === void 0 ? "unknown" : at === "gone" ? "gone" : "live";
14623
+ }
14378
14624
  const pane = runtimePane(ctx, rec);
14379
14625
  const adapter = pane ? paneAdapter(ctx, rec) : void 0;
14380
14626
  if (!pane || !adapter) return "unknown";
@@ -14391,7 +14637,7 @@ function probeLiveness(ctx, rec) {
14391
14637
  function rebindUnit(ctx, ref) {
14392
14638
  const rec = resolveAgent(ctx.store, ref);
14393
14639
  assertHasRuntime(rec, ref);
14394
- const cur = storablePane(ctx.env ?? process.env);
14640
+ const cur = callingStorablePane(ctx);
14395
14641
  if (!cur) throw new Error("unit rebind needs to run inside a tmux or herdr pane — there is no pane to bind");
14396
14642
  const oldPane = runtimePane(ctx, rec);
14397
14643
  if (rec.pane?.mux === cur.mux && rec.pane.id === cur.pane && ctx.store.resolvePaneId(cur.pane) === rec.id) return rec;
@@ -14399,13 +14645,15 @@ function rebindUnit(ctx, ref) {
14399
14645
  const holder = holderId && holderId !== rec.id ? loadAgent(ctx.store, holderId) : void 0;
14400
14646
  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`);
14401
14647
  const liveness = probeLiveness(ctx, rec);
14402
- 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})`);
14648
+ const ownTerminal = !!cur.terminal && rec.pane?.mux === "herdr" && rec.pane.terminal === cur.terminal;
14649
+ 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})`);
14403
14650
  const bound = {
14404
14651
  ...rec,
14405
14652
  status: "active",
14406
14653
  pane: {
14407
14654
  mux: cur.mux,
14408
- id: cur.pane
14655
+ id: cur.pane,
14656
+ ...cur.terminal ? { terminal: cur.terminal } : {}
14409
14657
  },
14410
14658
  lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
14411
14659
  };
@@ -14433,7 +14681,7 @@ function controlsFor(ctx, rec, liveness) {
14433
14681
  if (hasHonestReset(rec.harness)) controls.push("clear");
14434
14682
  }
14435
14683
  if (runtimePane(ctx, rec) !== void 0 && (liveness === "live" || liveness === "gone" || liveness === "unknown")) controls.push("stop");
14436
- if (rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
14684
+ if (liveness !== "exited" && rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
14437
14685
  if (liveness === "stopped" || liveness === "exited" || liveness === "gone") controls.push("rebind");
14438
14686
  controls.push("close");
14439
14687
  return controls;
@@ -14460,6 +14708,7 @@ function showUnit(ctx, ref) {
14460
14708
  } : null,
14461
14709
  cwd: rec.cwd,
14462
14710
  worktree: rec.worktree ?? null,
14711
+ sharedWorktree: rec.sharedWorktree ?? null,
14463
14712
  lastSeen: rec.lastSeen,
14464
14713
  controls: controlsFor(ctx, rec, liveness)
14465
14714
  };
@@ -14487,8 +14736,7 @@ function recordConversation(ctx, hookInput) {
14487
14736
  conversation: id
14488
14737
  });
14489
14738
  }
14490
- //#endregion
14491
- //#region src/wake/await.ts
14739
+ // src/wake/await.ts
14492
14740
  /** Internal poll interval — not user-facing (the round-trip cost of one `mail await` cycle). */
14493
14741
  const POLL_MS$1 = 1e3;
14494
14742
  const DEFAULT_TIMEOUT_MS = 6e5;
@@ -14533,8 +14781,7 @@ async function awaitReply(ctx, input) {
14533
14781
  await sleep(POLL_MS$1);
14534
14782
  }
14535
14783
  }
14536
- //#endregion
14537
- //#region src/wake/watch.ts
14784
+ // src/wake/watch.ts
14538
14785
  const POLL_MS = 1e3;
14539
14786
  /**
14540
14787
  * A continuous foreground observer: prints each NEW matching message as it arrives, polling on a
@@ -14566,8 +14813,7 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
14566
14813
  await sleep(POLL_MS);
14567
14814
  }
14568
14815
  }
14569
- //#endregion
14570
- //#region src/cli.ts
14816
+ // src/cli.ts
14571
14817
  const VERSION = globalThis.__SKILL_SCRIPT__?.version ?? JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
14572
14818
  function ctxOf(opts) {
14573
14819
  return {
@@ -14759,6 +15005,10 @@ function runWho(opts) {
14759
15005
  {
14760
15006
  key: "pane",
14761
15007
  get: (a) => a.pane?.id ?? "-"
15008
+ },
15009
+ {
15010
+ key: "shared",
15011
+ get: (a) => a.sharedWorktree?.root ?? "-"
14762
15012
  }
14763
15013
  ], `${agents.length} units`),
14764
15014
  json: agents
@@ -14766,18 +15016,25 @@ function runWho(opts) {
14766
15016
  if (agents.length === 0) nextStep("cyberlegion unit register to join");
14767
15017
  }
14768
15018
  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);
14769
- syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and sweep").action((opts) => {
15019
+ syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and delete exited units with no worktree on disk").action((opts) => {
14770
15020
  const ctx = ctxOf(opts);
14771
15021
  touch(ctx);
14772
15022
  const changed = prune(ctx);
14773
15023
  emit(formatOf(opts), {
14774
- toon: toonList("pruned", changed, [{
14775
- key: "id",
14776
- get: (a) => a.id
14777
- }, {
14778
- key: "handle",
14779
- get: (a) => a.handle
14780
- }], `${changed.length} pruned`),
15024
+ toon: toonList("pruned", changed, [
15025
+ {
15026
+ key: "id",
15027
+ get: (a) => a.id
15028
+ },
15029
+ {
15030
+ key: "handle",
15031
+ get: (a) => a.handle
15032
+ },
15033
+ {
15034
+ key: "pruned",
15035
+ get: (a) => a.pruned
15036
+ }
15037
+ ], `${changed.length} pruned (${changed.filter((a) => a.pruned === "deleted").length} deleted)`),
14781
15038
  json: changed
14782
15039
  });
14783
15040
  });
@@ -14958,6 +15215,7 @@ withGlobals(unit.command("show")).description("show a unit's runtime: where it i
14958
15215
  pane: view.pane ? `${view.pane.mux}:${view.pane.id}` : "-",
14959
15216
  cwd: view.cwd,
14960
15217
  worktree: view.worktree?.root ?? "-",
15218
+ shared: view.sharedWorktree?.root ?? "-",
14961
15219
  lastSeen: view.lastSeen,
14962
15220
  controls: view.controls.join(",") || "-"
14963
15221
  }),
@@ -15763,5 +16021,4 @@ async function runCli(argv = process.argv) {
15763
16021
  process.exit(1);
15764
16022
  }
15765
16023
  }
15766
- //#endregion
15767
16024
  export { runCli };