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/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { acquire, gitWorktreeCreator, primaryRoot, release } from "cyber-agentki
6
6
  import { createHash, randomBytes } from "node:crypto";
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { homedir } from "node:os";
9
- //#region src/mux-env.ts
9
+ // src/mux-env.ts
10
10
  /**
11
11
  * Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
12
12
  * is still honored"). cyber-mux's own fast-path reads `$CYBER_MUX`/`$CYBER_MUX_PANE` — its
@@ -35,8 +35,7 @@ function normalizeMuxEnv(env) {
35
35
  CYBER_MUX_PANE: env.CYBERLEGION_MUX_PANE
36
36
  };
37
37
  }
38
- //#endregion
39
- //#region src/paths.ts
38
+ // src/paths.ts
40
39
  /** The tracked marker file that makes a hub root initialized (see ensureMarker). */
41
40
  const MARKER_FILE = "config.json";
42
41
  /** Where `spawn` stamps that marker inside a worktree it creates, relative to the worktree root. */
@@ -166,8 +165,7 @@ const paths = {
166
165
  function sanitizePane(pane) {
167
166
  return pane.replace(/[^A-Za-z0-9_-]/g, "_");
168
167
  }
169
- //#endregion
170
- //#region src/identity.ts
168
+ // src/identity.ts
171
169
  const realExec = nodeExec;
172
170
  /** `currentPane`, narrowed to a backend the registry can actually store a locator under. A caller
173
171
  * inside a pane-carrying-but-unstorable multiplexer (wezterm/zellij) is simply unpaned here — it can
@@ -180,6 +178,128 @@ function storablePane(env) {
180
178
  pane: cur.pane
181
179
  } : void 0;
182
180
  }
181
+ /**
182
+ * This session's own pane as the multiplexer sees it now. The env names the pane the shell was
183
+ * launched in, and for herdr that goes stale: herdr gives a pane a new id when it moves to another
184
+ * tab or workspace, while the shell keeps its launch-time `$HERDR_PANE_ID`, and herdr ids recur on a
185
+ * hub, so the stale id can be another unit's pane (#154). herdr's `pane current` resolves the
186
+ * calling pane itself, so its answer wins on a herdr that answers it truly (0.8.2 on). When herdr
187
+ * gives no answer, or an older herdr disagrees with the env, the env id stands. tmux pane
188
+ * ids last as long as the pane, so tmux needs no second opinion.
189
+ */
190
+ function selfPane(cur, exec) {
191
+ if (cur?.mux !== "herdr") return cur;
192
+ const live = herdrCallingPane(exec);
193
+ if (!live) return cur;
194
+ if (live.pane === cur.pane) return {
195
+ ...cur,
196
+ ...live.terminal ? { terminal: live.terminal } : {}
197
+ };
198
+ if (!herdrReportsCallingPane(exec)) return cur;
199
+ return {
200
+ ...cur,
201
+ pane: live.pane,
202
+ ...live.terminal ? { terminal: live.terminal } : {}
203
+ };
204
+ }
205
+ /** herdr 0.8.2 fixed `pane current` to answer the calling pane; before it, the command answered
206
+ * another client's focused pane (herdr #2297), which would misattribute exactly as a stale env id
207
+ * does. A version that cannot be read is treated as too old. */
208
+ function herdrReportsCallingPane(exec) {
209
+ const m = exec("herdr", ["--version"])?.match(/(\d+)\.(\d+)\.(\d+)/);
210
+ if (!m) return false;
211
+ const [major, minor, patch] = m.slice(1).map(Number);
212
+ return major > 0 || minor > 8 || minor === 8 && patch >= 2;
213
+ }
214
+ function herdrCallingPane(exec) {
215
+ return herdrPaneAnswer(exec("herdr", ["pane", "current"]));
216
+ }
217
+ /** Parse a herdr `pane current` / `pane get` answer into its pane id and terminal id. */
218
+ function herdrPaneAnswer(out) {
219
+ if (!out) return void 0;
220
+ try {
221
+ const pane = JSON.parse(out)?.result?.pane;
222
+ const id = pane?.pane_id;
223
+ if (typeof id !== "string" || !id) return void 0;
224
+ const terminal = pane.terminal_id;
225
+ return {
226
+ pane: id,
227
+ ...typeof terminal === "string" && terminal ? { terminal } : {}
228
+ };
229
+ } catch {
230
+ return;
231
+ }
232
+ }
233
+ /**
234
+ * The herdr terminal id behind a pane id — what a unit's herdr pane record keys on besides the pane
235
+ * id. herdr gives a pane a new id when it moves to another workspace and keeps its terminal id
236
+ * (#272; herdr 0.9.5: `pane move wRD:p2 --tab wRE:t1` answered `wRE:p2` on the same `terminal_id`).
237
+ * Undefined when herdr does not answer or reports no terminal id.
238
+ */
239
+ function herdrTerminalOf(exec, paneId) {
240
+ return herdrPaneAnswer(exec("herdr", [
241
+ "pane",
242
+ "get",
243
+ paneId
244
+ ]))?.terminal;
245
+ }
246
+ /** The pane locator for a pane a spawn or restart just opened: tagged with its multiplexer so the
247
+ * unit's own `prune` runs the right liveness check, and for herdr carrying the terminal id a move
248
+ * keeps. `null` for a backend a record cannot carry. */
249
+ function openedPaneLocator(exec, mux, id) {
250
+ if (mux === "tmux") return {
251
+ mux,
252
+ id
253
+ };
254
+ if (mux !== "herdr") return null;
255
+ const terminal = herdrTerminalOf(exec, id);
256
+ return {
257
+ mux,
258
+ id,
259
+ ...terminal ? { terminal } : {}
260
+ };
261
+ }
262
+ /** herdr's live panes keyed by terminal id → current pane id. Empty when herdr gives no list, which
263
+ * says nothing about any pane. */
264
+ function herdrTerminals(exec) {
265
+ const map = /* @__PURE__ */ new Map();
266
+ const out = exec("herdr", ["pane", "list"]);
267
+ if (!out) return map;
268
+ try {
269
+ const panes = JSON.parse(out)?.result?.panes;
270
+ if (!Array.isArray(panes)) return map;
271
+ for (const p of panes) if (typeof p?.terminal_id === "string" && typeof p?.pane_id === "string") map.set(p.terminal_id, p.pane_id);
272
+ } catch {
273
+ return map;
274
+ }
275
+ return map;
276
+ }
277
+ /**
278
+ * Where a herdr record that carries its terminal id lives now: the pane id herdr lists for that
279
+ * terminal (`{ id }`), `'gone'` when herdr answered with a list that lacks the terminal, or
280
+ * `undefined` when herdr gave no list. Not called for a record without a terminal id, which keeps
281
+ * being judged by its pane id.
282
+ */
283
+ function locateTerminal(exec, terminal, cache = {}) {
284
+ cache.terminals ??= herdrTerminals(exec);
285
+ if (cache.terminals.size === 0) return void 0;
286
+ const id = cache.terminals.get(terminal);
287
+ return id ? { id } : "gone";
288
+ }
289
+ /** Point a record whose herdr pane moved at the pane id it has now, moving its pane pointer with it.
290
+ * The old pointer is dropped only while it still names this record, since herdr hands old ids out
291
+ * again. */
292
+ function followMove(ctx, rec, id) {
293
+ const pane = rec.pane;
294
+ if (!pane || pane.id === id) return;
295
+ if (ctx.store.resolvePaneId(pane.id) === rec.id) ctx.store.removePaneIndex(pane.id);
296
+ rec.pane = {
297
+ ...pane,
298
+ id
299
+ };
300
+ saveAgent(ctx.store, rec);
301
+ ctx.store.putPaneIndex(id, rec.id);
302
+ }
183
303
  const nowIso = (ctx) => new Date(ctx.now?.() ?? Date.now()).toISOString();
184
304
  function randomId() {
185
305
  return randomBytes(8).toString("hex");
@@ -225,10 +345,33 @@ function detectHarness(explicit, ctx) {
225
345
  * explicit via the env var.
226
346
  */
227
347
  function resolveSelfId(ctx) {
348
+ return resolveSelf(ctx).id;
349
+ }
350
+ function resolveSelf(ctx) {
228
351
  const env = ctx.env ?? process.env;
229
- const cur = currentPane(normalizeMuxEnv(env));
230
- if (cur) return ctx.store.resolvePaneId(cur.pane);
231
- return env.CYBERLEGION_AGENT_ID || void 0;
352
+ const cur = selfPane(currentPane(normalizeMuxEnv(env)), ctx.exec ?? realExec);
353
+ if (cur) return {
354
+ id: selfIdAt(ctx.store, cur),
355
+ cur
356
+ };
357
+ return {
358
+ id: env.CYBERLEGION_AGENT_ID || void 0,
359
+ cur
360
+ };
361
+ }
362
+ /**
363
+ * The unit a pane belongs to. The pane pointer answers first. When herdr named the calling pane's
364
+ * terminal, a pointer whose record carries a different terminal is stale (the id was handed to
365
+ * this pane after the unit's own pane moved away), and a missing pointer may mean this pane moved:
366
+ * either way the record bearing this terminal is the caller (#272). A record without a terminal id
367
+ * is trusted by its pointer as before.
368
+ */
369
+ function selfIdAt(store, cur) {
370
+ const indexed = store.resolvePaneId(cur.pane);
371
+ if (!cur.terminal) return indexed;
372
+ const terminal = indexed ? loadAgent(store, indexed)?.pane?.terminal : void 0;
373
+ if (indexed && (!terminal || terminal === cur.terminal)) return indexed;
374
+ return listAgents(store).find((a) => a.pane?.mux === "herdr" && a.pane.terminal === cur.terminal)?.id;
232
375
  }
233
376
  /** Register (or idempotently refresh) this session's identity. */
234
377
  function register(ctx, input) {
@@ -240,14 +383,14 @@ function register(ctx, input) {
240
383
  const existing = existingId ? loadAgent(ctx.store, existingId) : void 0;
241
384
  const id = existing?.id ?? existingId ?? randomId();
242
385
  const ts = nowIso(ctx);
243
- const cur = storablePane(env);
244
386
  const exec = ctx.exec ?? realExec;
387
+ const cur = selfPane(storablePane(env), exec);
245
388
  const rec = {
246
389
  id,
247
390
  handle: input.handle ?? existing?.handle ?? id.slice(0, 6),
248
391
  harness,
249
392
  cwd: process.cwd(),
250
- worktree: existing?.worktree ? keptWorktree(existing.worktree, exec) : gitWorktree(exec),
393
+ ...checkoutFor(ctx.store, exec, id, existing),
251
394
  pane: cur ? paneLocator(cur, exec, env) : null,
252
395
  status: "active",
253
396
  createdAt: existing?.createdAt ?? ts,
@@ -276,27 +419,75 @@ function paneLocator(cur, exec, env) {
276
419
  };
277
420
  return {
278
421
  mux: cur.mux,
279
- id: cur.pane
422
+ id: cur.pane,
423
+ ...cur.terminal ? { terminal: cur.terminal } : {}
280
424
  };
281
425
  }
282
- /** The checkout the cwd sits in: its root, its branch, and the repository's primary checkout — the
283
- * same `primaryRoot` spawn records, so a lookup by repository finds a unit in a linked worktree by
284
- * the repository's name rather than the worktree folder's. */
285
- function gitWorktree(exec) {
286
- const root = exec("git", ["rev-parse", "--show-toplevel"]);
426
+ /** The checkout `dir` (else the cwd) sits in: its root, its branch, and the repository's primary
427
+ * checkout — the same `primaryRoot` spawn records, so a lookup by repository finds a unit in a linked
428
+ * worktree by the repository's name rather than the worktree folder's. */
429
+ function gitWorktree(exec, dir) {
430
+ const git = dir === void 0 ? exec : (cmd, args) => exec(cmd, cmd === "git" ? [
431
+ "-C",
432
+ dir,
433
+ ...args
434
+ ] : args);
435
+ const root = git("git", ["rev-parse", "--show-toplevel"]);
287
436
  if (!root) return null;
288
- const branch = exec("git", [
437
+ const branch = git("git", [
289
438
  "rev-parse",
290
439
  "--abbrev-ref",
291
440
  "HEAD"
292
441
  ]) ?? void 0;
293
- const primaryRoot = primaryRootHere(exec);
442
+ const primaryRoot = primaryRootHere(git);
294
443
  return {
295
444
  root,
296
445
  ...branch !== void 0 ? { branch } : {},
297
446
  ...primaryRoot !== void 0 ? { primaryRoot } : {}
298
447
  };
299
448
  }
449
+ /** The id of the unit, other than `self`, whose record owns the worktree at `root`. */
450
+ function ownerOf(store, root, self) {
451
+ const at = resolve(root);
452
+ return listAgents(store).find((a) => a.id !== self && a.worktree?.root && resolve(a.worktree.root) === at)?.id;
453
+ }
454
+ /** `checkout` as a shared reference, naming the unit that owns it when one does. */
455
+ function asShared(store, checkout, self) {
456
+ const owner = ownerOf(store, checkout.root, self);
457
+ return {
458
+ ...checkout,
459
+ ...owner ? { owner } : {}
460
+ };
461
+ }
462
+ /** The checkout a `--cwd` spawn opens into, as a shared reference — spawn created none, so the unit
463
+ * owns none, whoever does. `undefined` when `dir` is in no git checkout. */
464
+ function sharedCheckoutAt(store, exec, dir) {
465
+ const checkout = gitWorktree(exec, dir);
466
+ return checkout ? asShared(store, checkout) : void 0;
467
+ }
468
+ /**
469
+ * Which checkout a registering session owns and which it only shares. An existing owned worktree is
470
+ * kept. Otherwise the cwd's checkout is the session's own — unless another unit's record owns it, or
471
+ * this unit already records it as shared (a `--cwd` spawn): then it is a shared reference, and the
472
+ * session owns none, so its close can never remove another unit's worktree or release its lease.
473
+ */
474
+ function checkoutFor(store, exec, id, existing) {
475
+ const kept = existing?.sharedWorktree ? { sharedWorktree: existing.sharedWorktree } : {};
476
+ if (existing?.worktree) return {
477
+ worktree: keptWorktree(existing.worktree, exec),
478
+ ...kept
479
+ };
480
+ const here = gitWorktree(exec);
481
+ if (!here) return {
482
+ worktree: null,
483
+ ...kept
484
+ };
485
+ if (existing?.sharedWorktree || ownerOf(store, here.root, id)) return {
486
+ worktree: null,
487
+ sharedWorktree: asShared(store, here, id)
488
+ };
489
+ return { worktree: here };
490
+ }
300
491
  function primaryRootHere(exec) {
301
492
  try {
302
493
  return resolvePrimaryRoot$1(exec);
@@ -377,10 +568,26 @@ function bumpLastSeen(ctx, id) {
377
568
  rec.lastSeen = nowIso(ctx);
378
569
  saveAgent(ctx.store, rec);
379
570
  }
380
- /** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
571
+ /**
572
+ * Refresh this session's own last-seen if it is registered — best-effort, never throws. In a herdr
573
+ * pane whose calling pane herdr reported, it also heals the record's pane to the id and terminal
574
+ * herdr gave, so a unit whose pane moved fixes its own record on its next turn (#272).
575
+ */
381
576
  function touch(ctx) {
382
- const id = resolveSelfId(ctx);
383
- if (id) bumpLastSeen(ctx, id);
577
+ const { id, cur } = resolveSelf(ctx);
578
+ if (!id) return;
579
+ bumpLastSeen(ctx, id);
580
+ if (cur?.mux !== "herdr" || !cur.terminal) return;
581
+ const rec = loadAgent(ctx.store, id);
582
+ if (rec?.pane?.mux !== "herdr") return;
583
+ if (rec.pane.terminal !== cur.terminal) {
584
+ rec.pane = {
585
+ ...rec.pane,
586
+ terminal: cur.terminal
587
+ };
588
+ saveAgent(ctx.store, rec);
589
+ }
590
+ followMove(ctx, rec, cur.pane);
384
591
  }
385
592
  const STALE_MS = 9e5;
386
593
  /** Standing and service records have no session of their own, so nothing about a pane or a timer
@@ -407,7 +614,9 @@ function isPrunable(rec) {
407
614
  function sessionLive(ctx, rec) {
408
615
  if (rec.status === "exited" || rec.status === "stopped") return false;
409
616
  if (!rec.pane) return true;
410
- const panes = PANE_ADAPTERS[rec.pane.mux].listPanes(ctx.exec ?? realExec);
617
+ const exec = ctx.exec ?? realExec;
618
+ if (rec.pane.mux === "herdr" && rec.pane.terminal) return locateTerminal(exec, rec.pane.terminal) !== "gone";
619
+ const panes = PANE_ADAPTERS[rec.pane.mux].listPanes(exec);
411
620
  if (panes.length === 0) return true;
412
621
  const id = rec.pane.id;
413
622
  return panes.some((p) => p.id === id);
@@ -482,11 +691,12 @@ function reconcile(ctx, opts) {
482
691
  if (panes.length === 0) return [];
483
692
  const live = new Set(panes.map((p) => p.id));
484
693
  const changed = [];
694
+ const cache = {};
485
695
  for (const rec of listAgents(ctx.store)) {
486
696
  if (!isPrunable(rec)) continue;
487
697
  if (!rec.pane) continue;
488
698
  if (rec.pane.mux !== cur.mux) continue;
489
- if (!live.has(rec.pane.id)) {
699
+ if (rec.pane.mux === "herdr" && rec.pane.terminal ? terminalGone(ctx, rec, rec.pane.terminal, cache) : !live.has(rec.pane.id)) {
490
700
  rec.status = "exited";
491
701
  saveAgent(ctx.store, rec);
492
702
  changed.push(rec);
@@ -511,27 +721,93 @@ function paneGone(exec, pane, lists) {
511
721
  }
512
722
  return panes.length > 0 && !panes.some((p) => p.id === pane.id);
513
723
  }
514
- /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
515
- * first, then judges each remaining record (covers the other mux and sessions outside any
516
- * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
517
- * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
724
+ /**
725
+ * Whether a herdr record that carries its terminal id is positively gone: herdr listed its panes and
726
+ * that terminal is not among them. A live terminal under a new pane id is a moved pane, so the record
727
+ * follows it instead (#272). A pane id that herdr now gives another terminal does not keep the record
728
+ * alive, since herdr ids recur. No list at all decides nothing.
729
+ */
730
+ function terminalGone(ctx, rec, terminal, cache) {
731
+ const at = locateTerminal(ctx.exec ?? realExec, terminal, cache);
732
+ if (at === "gone") return true;
733
+ if (at) followMove(ctx, rec, at.id);
734
+ return false;
735
+ }
736
+ /**
737
+ * Mark agents whose session is gone as exited, then delete every exited unit with nothing left to
738
+ * work from. Reconcile-culls against the current mux's live set first, then judges each remaining
739
+ * record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
740
+ * pane alone — a live or unprobeable pane keeps it whatever its last-seen — and a pane-less record by
741
+ * the staleness timer, its only signal. An exited unit whose worktree is still on disk is kept, since
742
+ * `unit close` owns removing that worktree; every other exited unit is deleted with `deleteExited`.
743
+ */
518
744
  function prune(ctx) {
519
745
  const exec = ctx.exec ?? realExec;
520
746
  const now = ctx.now?.() ?? Date.now();
521
- const changed = reconcile(ctx);
747
+ const marked = reconcile(ctx);
522
748
  const lists = /* @__PURE__ */ new Map();
749
+ const cache = {};
523
750
  for (const rec of listAgents(ctx.store)) {
524
751
  if (!isPrunable(rec)) continue;
525
- if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
752
+ 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)) {
526
753
  rec.status = "exited";
527
754
  saveAgent(ctx.store, rec);
528
- changed.push(rec);
755
+ marked.push(rec);
529
756
  }
530
757
  }
531
- return changed;
758
+ const deleted = deleteExited(ctx);
759
+ const gone = new Set(deleted.map((r) => r.id));
760
+ return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
761
+ ...r,
762
+ pruned: "exited"
763
+ })), ...deleted.map((r) => ({
764
+ ...r,
765
+ pruned: "deleted"
766
+ }))];
767
+ }
768
+ /** Whether an exited record still has a worktree on disk — the one thing it leaves behind that
769
+ * `unit close` must deal with. A record that never had a worktree has nothing on disk. */
770
+ function worktreeOnDisk(rec) {
771
+ if (rec.worktree?.root == null || !existsSync(rec.worktree.root)) return false;
772
+ return !isPrimaryCheckout(rec.worktree.root, rec.worktree.primaryRoot);
773
+ }
774
+ /** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
775
+ * `unit prune` share for "a seat on the main checkout, which nothing removes". */
776
+ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
777
+ return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
778
+ }
779
+ /**
780
+ * 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
781
+ * pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
782
+ * stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
783
+ * resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
784
+ * since. A worktree that is gone from disk may still be registered with git, and the record is the
785
+ * last place that names its repository, so that registration is cleared first, best-effort, as
786
+ * `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
787
+ * someone else.
788
+ */
789
+ function deleteExited(ctx) {
790
+ const exec = ctx.exec ?? realExec;
791
+ const deleted = [];
792
+ for (const rec of listAgents(ctx.store)) {
793
+ if (rec.status !== "exited" || isSessionIndependent(rec) || worktreeOnDisk(rec)) continue;
794
+ const primaryRoot = rec.worktree?.primaryRoot;
795
+ if (primaryRoot && existsSync(primaryRoot)) exec("git", [
796
+ "-C",
797
+ primaryRoot,
798
+ "worktree",
799
+ "prune"
800
+ ]);
801
+ const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(rec.id);
802
+ if (pane && ctx.store.resolvePaneId(pane) === rec.id) ctx.store.removePaneIndex(pane);
803
+ ctx.store.removeAgent(rec.id);
804
+ ctx.store.removeAgentData(rec.id);
805
+ ctx.store.removeMailbox(rec.id);
806
+ deleted.push(rec);
807
+ }
808
+ return deleted;
532
809
  }
533
- //#endregion
534
- //#region src/console/prompt-guard.ts
810
+ // src/console/prompt-guard.ts
535
811
  const CODEX_PLACEHOLDERS = /* @__PURE__ */ new Set([
536
812
  "Ask Codex to do anything",
537
813
  "Explain this codebase",
@@ -689,8 +965,7 @@ async function clearLine(adapter, exec, target, sleep) {
689
965
  adapter.sendText(exec, target, KILL_LINE);
690
966
  await sleep(KEY_SETTLE_MS);
691
967
  }
692
- //#endregion
693
- //#region src/console/ring.ts
968
+ // src/console/ring.ts
694
969
  const DEFAULT_ATTEMPTS = 10;
695
970
  const DEFAULT_SETTLE_MS = 400;
696
971
  /** cyber-mux's `isStaged` matches on the same prefix length. */
@@ -887,8 +1162,7 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
887
1162
  };
888
1163
  }
889
1164
  }
890
- //#endregion
891
- //#region src/console/trust.ts
1165
+ // src/console/trust.ts
892
1166
  /**
893
1167
  * Each harness's folder-trust prompt, as it draws it, and the keys that accept it. Read from Claude
894
1168
  * Code 2.1.286, Codex 0.153.4 (and its newer "Trust this folder?" wording), and cursor-agent
@@ -998,8 +1272,7 @@ function stuck(where) {
998
1272
  message: `${where}, and the prompt is still showing after spawn sent the keys that accept it. A person must answer it.`
999
1273
  };
1000
1274
  }
1001
- //#endregion
1002
- //#region src/mux-select.ts
1275
+ // src/mux-select.ts
1003
1276
  /**
1004
1277
  * Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
1005
1278
  * `$CYBERLEGION_MUX*` → `$CYBER_MUX*` env seam (`mux-env.ts`) — tmux/herdr map to their existing
@@ -1019,8 +1292,7 @@ function selectSessionAdapter(env, exec = realExec) {
1019
1292
  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`);
1020
1293
  throw new Error("spawn requires a session backend — run inside tmux ($TMUX) or herdr ($HERDR_ENV=1)");
1021
1294
  }
1022
- //#endregion
1023
- //#region src/workspace-label.ts
1295
+ // src/workspace-label.ts
1024
1296
  /** Leading actions that tear down or revert — the `A2-` class, checked first. */
1025
1297
  const TEARDOWN_ACTIONS = /* @__PURE__ */ new Set([
1026
1298
  "clean",
@@ -1163,8 +1435,7 @@ function fitWords(words, budget) {
1163
1435
  }
1164
1436
  return out;
1165
1437
  }
1166
- //#endregion
1167
- //#region src/worktree-lease.ts
1438
+ // src/worktree-lease.ts
1168
1439
  /**
1169
1440
  * The two worktree-library calls a unit's lifetime makes: `unit spawn` acquires a worktree, `unit
1170
1441
  * close` releases it. Gathered here so a test can stand in for them and drive spawn's routing without
@@ -1188,8 +1459,7 @@ function leaseHolder(id) {
1188
1459
  * cyberlegion's own file, not the unit's work, and counting it would keep every slot from reuse in a
1189
1460
  * project that does not track it. */
1190
1461
  const LEASE_IGNORE = [WORKTREE_MARKER];
1191
- //#endregion
1192
- //#region src/session.ts
1462
+ // src/session.ts
1193
1463
  /** How each harness's own CLI is launched in the new pane. */
1194
1464
  const LAUNCH_MAP = {
1195
1465
  claude: "claude",
@@ -1282,6 +1552,7 @@ async function spawn(ctx, input) {
1282
1552
  const from = callerPane(sessionAdapter, normalizedEnv);
1283
1553
  let cwd;
1284
1554
  let worktree;
1555
+ let sharedWorktree;
1285
1556
  let base;
1286
1557
  let reused;
1287
1558
  let target;
@@ -1290,6 +1561,7 @@ async function spawn(ctx, input) {
1290
1561
  cwd = resolve(input.cwd);
1291
1562
  if (primaryRoot$1 !== void 0) assertDistinctFromPrimary$1(cwd, primaryRoot$1);
1292
1563
  worktree = null;
1564
+ sharedWorktree = sharedCheckoutAt(ctx.store, exec, cwd);
1293
1565
  const at = input.at ?? "tab";
1294
1566
  target = sessionAdapter.open(exec, {
1295
1567
  cwd,
@@ -1380,10 +1652,8 @@ async function spawn(ctx, input) {
1380
1652
  harness,
1381
1653
  cwd,
1382
1654
  worktree,
1383
- pane: muxName === "tmux" || muxName === "herdr" ? {
1384
- mux: muxName,
1385
- id: target.id
1386
- } : null,
1655
+ ...sharedWorktree ? { sharedWorktree } : {},
1656
+ pane: openedPaneLocator(exec, muxName, target.id),
1387
1657
  status: "active",
1388
1658
  createdAt: ts,
1389
1659
  lastSeen: ts,
@@ -1578,8 +1848,7 @@ function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
1578
1848
  if (input.task != null && input.task !== "") return input.task;
1579
1849
  return null;
1580
1850
  }
1581
- //#endregion
1582
- //#region src/agentdef/resolve.ts
1851
+ // src/agentdef/resolve.ts
1583
1852
  function parseAgentDefFile(text) {
1584
1853
  const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
1585
1854
  if (!m) return {
@@ -1699,8 +1968,7 @@ function listAgentDefs(input = {}) {
1699
1968
  }
1700
1969
  return defs;
1701
1970
  }
1702
- //#endregion
1703
- //#region src/agentdef/realize.ts
1971
+ // src/agentdef/realize.ts
1704
1972
  const DEFAULT_HARNESS = "claude";
1705
1973
  /** POSIX single-quote a value for safe inclusion in a shell command line. */
1706
1974
  function shellQuote(value) {
@@ -1785,8 +2053,7 @@ function realizeLaunch(def, opts = {}) {
1785
2053
  ...briefInstructions ? { briefInstructions } : {}
1786
2054
  };
1787
2055
  }
1788
- //#endregion
1789
- //#region src/message.ts
2056
+ // src/message.ts
1790
2057
  /** Write one message into the recipient's inbox. Collision-free by <epochMs>-<hex>. */
1791
2058
  function send(ctx, input) {
1792
2059
  const toId = resolveRecipient(ctx.store, input.to);
@@ -1847,8 +2114,7 @@ function deleteMessage(ctx, meId, msgId) {
1847
2114
  function deleteMailbox(store, id) {
1848
2115
  store.removeMailbox(id);
1849
2116
  }
1850
- //#endregion
1851
- //#region src/decommission.ts
2117
+ // src/decommission.ts
1852
2118
  /**
1853
2119
  * Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
1854
2120
  * dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
@@ -1884,7 +2150,7 @@ async function decommission(ctx, input) {
1884
2150
  const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
1885
2151
  const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
1886
2152
  const held = rec.worktree?.lease;
1887
- const onPrimary = worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
2153
+ const onPrimary = isPrimaryCheckout(worktreeRoot, primaryRoot);
1888
2154
  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`);
1889
2155
  const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
1890
2156
  if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
@@ -1950,10 +2216,9 @@ function isDirty(exec, worktreeRoot) {
1950
2216
  "--porcelain",
1951
2217
  "--untracked-files=all"
1952
2218
  ]);
1953
- return !!status && status.split("\n").some((line) => line && line !== `?? ${WORKTREE_MARKER}`);
2219
+ return !!status && status.split("\n").some((line) => line && line !== `?? .agents/cyberlegion/config.json`);
1954
2220
  }
1955
- //#endregion
1956
- //#region src/install.ts
2221
+ // src/install.ts
1957
2222
  const PIN_TOKEN = /^[0-9A-Za-z][0-9A-Za-z._+-]*$/;
1958
2223
  function validatePin(pin) {
1959
2224
  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)`);
@@ -2106,8 +2371,7 @@ function removeCursor(settings, event, canonical) {
2106
2371
  else delete hooks[event];
2107
2372
  return true;
2108
2373
  }
2109
- //#endregion
2110
- //#region src/output.ts
2374
+ // src/output.ts
2111
2375
  function stringifyCell(v) {
2112
2376
  if (v == null) return "";
2113
2377
  const s = String(v);
@@ -2140,8 +2404,7 @@ function fail(msg) {
2140
2404
  console.error(JSON.stringify({ error: msg }));
2141
2405
  process.exit(1);
2142
2406
  }
2143
- //#endregion
2144
- //#region src/project.ts
2407
+ // src/project.ts
2145
2408
  /**
2146
2409
  * The canonical git common dir for `dir`, or undefined outside a repository. The common dir is the
2147
2410
  * one path every checkout of a repository shares — the default checkout and each linked worktree
@@ -2260,8 +2523,7 @@ function byPath(ctx, dir) {
2260
2523
  if (!commonDir) throw new Error(`"${dir}" is not inside a git repository`);
2261
2524
  return ctx.store.getProject(projectIdOf(commonDir)) ?? registerProject(ctx, { dir });
2262
2525
  }
2263
- //#endregion
2264
- //#region src/runtime/inject-inbox.ts
2526
+ // src/runtime/inject-inbox.ts
2265
2527
  const EVENTS = ["SessionStart"];
2266
2528
  /**
2267
2529
  * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
@@ -2316,8 +2578,7 @@ function injectInbox(ctx, event) {
2316
2578
  additionalContext: parts.join("\n\n")
2317
2579
  } };
2318
2580
  }
2319
- //#endregion
2320
- //#region src/service.ts
2581
+ // src/service.ts
2321
2582
  /** A refused ownership transition. `stale` means the caller's generation, token, or holder claim is
2322
2583
  * not the current one — the caller is not (or no longer) the authority it claims to be. */
2323
2584
  var ServiceOwnershipError = class extends Error {
@@ -2634,8 +2895,7 @@ async function startService(ctx, projectRef, name, input) {
2634
2895
  throw err;
2635
2896
  }
2636
2897
  }
2637
- //#endregion
2638
- //#region src/store/errors.ts
2898
+ // src/store/errors.ts
2639
2899
  /** A record file exists but its content didn't parse as JSON — a torn write (crash mid-`writeFileSync`,
2640
2900
  * pre-atomic-write code path) or on-disk tampering. Carries the file path and the original parse
2641
2901
  * error so a caller can report exactly what's broken and where, rather than "Unexpected token" with
@@ -2648,8 +2908,7 @@ var CorruptRecordError = class extends Error {
2648
2908
  this.name = "CorruptRecordError";
2649
2909
  }
2650
2910
  };
2651
- //#endregion
2652
- //#region src/store/process-liveness.ts
2911
+ // src/store/process-liveness.ts
2653
2912
  /**
2654
2913
  * Probe whether `pid` is alive using `process.kill(pid, 0)` — the POSIX "does this process exist"
2655
2914
  * idiom; it sends no signal, it only tests deliverability. `ESRCH` (no such process) is the only
@@ -2669,8 +2928,7 @@ function probeProcess(pid) {
2669
2928
  return err.code === "ESRCH" ? "dead" : "unknown";
2670
2929
  }
2671
2930
  }
2672
- //#endregion
2673
- //#region src/store/lock.ts
2931
+ // src/store/lock.ts
2674
2932
  var LockTimeoutError = class extends Error {
2675
2933
  name;
2676
2934
  constructor(name) {
@@ -2807,8 +3065,7 @@ async function withLockAsync(root, name, fn, opts) {
2807
3065
  handle.release();
2808
3066
  }
2809
3067
  }
2810
- //#endregion
2811
- //#region src/store/file-store.ts
3068
+ // src/store/file-store.ts
2812
3069
  /** Parse a record file's content, wrapping a `JSON.parse` failure in a typed, file-named
2813
3070
  * `CorruptRecordError` instead of letting a bare `SyntaxError` bubble up from deep inside
2814
3071
  * `listInbox`/`listAgents`/`getAgent` with no indication of WHICH file broke. A file that doesn't
@@ -3018,8 +3275,7 @@ var FileStore = class {
3018
3275
  return withLockAsync(this.root, name, fn);
3019
3276
  }
3020
3277
  };
3021
- //#endregion
3022
- //#region src/wake/await.ts
3278
+ // src/wake/await.ts
3023
3279
  /** Internal poll interval — not user-facing (the round-trip cost of one `mail await` cycle). */
3024
3280
  const POLL_MS$1 = 1e3;
3025
3281
  const DEFAULT_TIMEOUT_MS = 6e5;
@@ -3064,8 +3320,7 @@ async function awaitReply(ctx, input) {
3064
3320
  await sleep(POLL_MS$1);
3065
3321
  }
3066
3322
  }
3067
- //#endregion
3068
- //#region src/wake/watch.ts
3323
+ // src/wake/watch.ts
3069
3324
  const POLL_MS = 1e3;
3070
3325
  /**
3071
3326
  * A continuous foreground observer: prints each NEW matching message as it arrives, polling on a
@@ -3097,5 +3352,4 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
3097
3352
  await sleep(POLL_MS);
3098
3353
  }
3099
3354
  }
3100
- //#endregion
3101
3355
  export { DEFAULT_RESERVATION_TTL_MS, FileStore, ServiceOwnershipError, ack, acquireService, assertDistinctFromPrimary, awaitReply, bindService, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, handoffService, inbox, injectInbox, install, listAgentDefs, listAgents, listProjects, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, registerProject, releaseService, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProject, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, resolveService, selectSessionAdapter, send, serviceEndpointId, spawn, spawnAndWake, startService, toonList, toonObject, touch, verifyOwnership, watchMail, withOwnership };