cyberlegion 1.1.0 → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberlegion",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberlegion",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
5
5
  "author": {
6
6
  "name": "unional"
package/.plugin/pins.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "cyberlegion": "1.1.0"
2
+ "cyberlegion": "1.2.0"
3
3
  }
package/dist/cli.mjs CHANGED
@@ -6139,6 +6139,7 @@ function reconcile(ctx, opts) {
6139
6139
  const cur = storablePane(ctx.env ?? process.env);
6140
6140
  if (!cur) return [];
6141
6141
  const panes = PANE_ADAPTERS[cur.mux].listPanes(exec);
6142
+ if (panes.length === 0) return [];
6142
6143
  const live = new Set(panes.map((p) => p.id));
6143
6144
  const changed = [];
6144
6145
  for (const rec of listAgents(ctx.store)) {
@@ -6154,18 +6155,34 @@ function reconcile(ctx, opts) {
6154
6155
  if (opts?.adopt) changed.push(...adopt(ctx, panes));
6155
6156
  return changed;
6156
6157
  }
6157
- /** Mark agents whose pane is gone or whose last-seen is stale as exited. Reconcile-culls against the
6158
- * current mux's live set first, then falls through to the per-record paneExists + staleness check
6159
- * (covers the other mux and sessions outside any multiplexer pane). */
6158
+ /**
6159
+ * Whether a record's pane is positively gone: its existence probe fails AND its mux answers with a
6160
+ * pane list that does not hold it. `paneExists` alone collapses "unreachable" into "gone", so a mux
6161
+ * that answers no list at all (server down, a failed query) leaves the pane undecided — never gone.
6162
+ * The list is read at most once per mux per call.
6163
+ */
6164
+ function paneGone(exec, pane, lists) {
6165
+ const adapter = PANE_ADAPTERS[pane.mux];
6166
+ if (adapter.paneExists(exec, { id: pane.id })) return false;
6167
+ let panes = lists.get(pane.mux);
6168
+ if (!panes) {
6169
+ panes = adapter.listPanes(exec);
6170
+ lists.set(pane.mux, panes);
6171
+ }
6172
+ return panes.length > 0 && !panes.some((p) => p.id === pane.id);
6173
+ }
6174
+ /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
6175
+ * first, then judges each remaining record (covers the other mux and sessions outside any
6176
+ * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
6177
+ * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
6160
6178
  function prune(ctx) {
6161
6179
  const exec = ctx.exec ?? realExec;
6162
6180
  const now = ctx.now?.() ?? Date.now();
6163
6181
  const changed = reconcile(ctx);
6182
+ const lists = /* @__PURE__ */ new Map();
6164
6183
  for (const rec of listAgents(ctx.store)) {
6165
6184
  if (!isPrunable(rec)) continue;
6166
- const paneGone = rec.pane ? !PANE_ADAPTERS[rec.pane.mux].paneExists(exec, { id: rec.pane.id }) : false;
6167
- const stale = now - new Date(rec.lastSeen).getTime() > STALE_MS;
6168
- if (paneGone || stale) {
6185
+ if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
6169
6186
  rec.status = "exited";
6170
6187
  saveAgent(ctx.store, rec);
6171
6188
  changed.push(rec);
@@ -6880,7 +6897,10 @@ function spawn(ctx, input) {
6880
6897
  assertDistinctFromPrimary(opened.worktree.root, primaryRoot);
6881
6898
  ensureMarker(join(opened.worktree.root, ".agents", "cyberlegion"));
6882
6899
  cwd = opened.worktree.root;
6883
- worktree = opened.worktree;
6900
+ worktree = {
6901
+ ...opened.worktree,
6902
+ primaryRoot
6903
+ };
6884
6904
  target = opened.target;
6885
6905
  } else {
6886
6906
  const added = gitWorktreeAdapter.add(exec, {
@@ -6891,7 +6911,10 @@ function spawn(ctx, input) {
6891
6911
  assertDistinctFromPrimary(added.root, primaryRoot);
6892
6912
  ensureMarker(join(added.root, ".agents", "cyberlegion"));
6893
6913
  cwd = added.root;
6894
- worktree = added;
6914
+ worktree = {
6915
+ ...added,
6916
+ primaryRoot
6917
+ };
6895
6918
  target = sessionAdapter.open(exec, {
6896
6919
  cwd,
6897
6920
  launch: launchLine(),
@@ -7584,12 +7607,18 @@ function decommission(ctx, input) {
7584
7607
  const exec = ctx.exec ?? realExec;
7585
7608
  const env = ctx.env ?? process.env;
7586
7609
  const worktreeRoot = rec.worktree?.root;
7587
- const primaryRoot = worktreeRoot ? resolvePrimaryRoot(exec) : void 0;
7588
- if (worktreeRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
7589
7610
  const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
7611
+ const primaryRoot = worktreeExists ? primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
7612
+ if (worktreeRoot && primaryRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
7590
7613
  const removesWorktree = worktreeExists && !input.keepWorktree;
7591
7614
  if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
7592
7615
  const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
7616
+ if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
7617
+ "-C",
7618
+ primaryRoot,
7619
+ "worktree",
7620
+ "prune"
7621
+ ]);
7593
7622
  if (removesWorktree) try {
7594
7623
  gitWorktreeAdapter.remove(exec, worktreeRoot, { primaryRoot });
7595
7624
  } catch (err) {
@@ -7610,6 +7639,22 @@ function decommission(ctx, input) {
7610
7639
  };
7611
7640
  }
7612
7641
  /**
7642
+ * The primary checkout of the repository `worktreeRoot` belongs to — `--git-common-dir` asked of the
7643
+ * worktree itself, so the answer does not depend on where the caller runs. Throws for a directory git
7644
+ * does not know: there is no repository to remove it from.
7645
+ */
7646
+ function primaryRootOf(exec, worktreeRoot) {
7647
+ const commonDir = exec("git", [
7648
+ "-C",
7649
+ worktreeRoot,
7650
+ "rev-parse",
7651
+ "--path-format=absolute",
7652
+ "--git-common-dir"
7653
+ ]);
7654
+ if (!commonDir) throw new Error(`cannot resolve the repository of worktree ${worktreeRoot} — git does not know it`);
7655
+ return dirname(commonDir);
7656
+ }
7657
+ /**
7613
7658
  * Whether the worktree holds work a removal would discard. The marker `spawn` stamps into every
7614
7659
  * worktree it creates is cyberlegion's own file, not the unit's work: in a project that does not
7615
7660
  * track it, it shows as untracked in every spawned worktree, and counting it would make a plain close
@@ -7644,10 +7689,7 @@ const VENDORS = {
7644
7689
  claude: {
7645
7690
  file: ".claude/settings.json",
7646
7691
  shape: "claude",
7647
- events: {
7648
- SessionStart: "SessionStart",
7649
- PostToolUse: "PostToolUse"
7650
- },
7692
+ events: { SessionStart: "SessionStart" },
7651
7693
  pluginHook: true
7652
7694
  },
7653
7695
  cursor: {
@@ -7659,13 +7701,11 @@ const VENDORS = {
7659
7701
  codex: {
7660
7702
  file: ".codex/hooks.json",
7661
7703
  shape: "cursor",
7662
- events: {
7663
- SessionStart: "SessionStart",
7664
- PostToolUse: "PostToolUse"
7665
- },
7704
+ events: { SessionStart: "SessionStart" },
7666
7705
  pluginHook: true
7667
7706
  }
7668
7707
  };
7708
+ const RETIRED_EVENTS = ["PostToolUse"];
7669
7709
  function readJson(file) {
7670
7710
  if (!existsSync(file)) return {};
7671
7711
  try {
@@ -7709,6 +7749,17 @@ function install(harness, projectDir = process.cwd(), pin) {
7709
7749
  status
7710
7750
  });
7711
7751
  }
7752
+ if (spec.pluginHook) for (const event of RETIRED_EVENTS) {
7753
+ if (!(spec.shape === "claude" ? removeClaude(settings, event, event) : removeCursor(settings, event, event))) continue;
7754
+ changed = true;
7755
+ results.push({
7756
+ harness,
7757
+ event,
7758
+ vendorEvent: event,
7759
+ file,
7760
+ status: "removed project hook"
7761
+ });
7762
+ }
7712
7763
  if (changed) writeJson$1(file, settings);
7713
7764
  return results;
7714
7765
  }
@@ -7723,12 +7774,10 @@ function upsertClaude(settings, event, command) {
7723
7774
  return "already present";
7724
7775
  }
7725
7776
  }
7726
- const group = { hooks: [{
7777
+ groups.push({ hooks: [{
7727
7778
  type: "command",
7728
7779
  command
7729
- }] };
7730
- if (event === "PostToolUse") group.matcher = "Write|Edit";
7731
- groups.push(group);
7780
+ }] });
7732
7781
  return "registered";
7733
7782
  }
7734
7783
  function upsertCursor(settings, event, command) {
@@ -7932,7 +7981,7 @@ function byPath(ctx, dir) {
7932
7981
  }
7933
7982
  //#endregion
7934
7983
  //#region src/runtime/inject-inbox.ts
7935
- const EVENTS = ["SessionStart", "PostToolUse"];
7984
+ const EVENTS = ["SessionStart"];
7936
7985
  /**
7937
7986
  * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
7938
7987
  * the hub's main pane), and return the SessionStart-style injection payload — or null when there is
@@ -7980,9 +8029,6 @@ function injectInbox(ctx, event) {
7980
8029
  }
7981
8030
  }
7982
8031
  } catch {}
7983
- if (rec && !rec.spawnedBy) try {
7984
- if (cur ? !ctx.store.getMainPane() : !listAgents(ctx.store).some((a) => a.kind === "standing")) parts.push("## Legion setup\n\nThis pane has no owner inbox bound yet — run `cyberlegion init` to register the surfacing hook and bind this pane as the owner live presence.");
7985
- } catch {}
7986
8032
  if (parts.length === 0) return null;
7987
8033
  return { hookSpecificOutput: {
7988
8034
  hookEventName: event,
@@ -9792,7 +9838,7 @@ withGlobals(mail.command("watch")).description("stream new matching mail as it a
9792
9838
  });
9793
9839
  });
9794
9840
  });
9795
- withGlobals(mail.command("hook")).description("emit the harness hook injection payload (raw JSON on stdout, not TOON)").option("--event <event>", "SessionStart | PostToolUse", "SessionStart").action((opts) => {
9841
+ withGlobals(mail.command("hook")).description("emit the harness hook injection payload (raw JSON on stdout, not TOON)").option("--event <event>", "SessionStart", "SessionStart").action((opts) => {
9796
9842
  const ctx = ctxOf(opts);
9797
9843
  touch(ctx);
9798
9844
  const payload = injectInbox(ctx, opts.event);
package/dist/index.d.mts CHANGED
@@ -25,9 +25,13 @@ interface AgentRecord {
25
25
  /** Absent for a standing record (a human/owner principal — no claude/cursor/codex harness). */
26
26
  harness?: Harness;
27
27
  cwd: string;
28
+ /** `primaryRoot` is the primary checkout of the repository the worktree belongs to, recorded by
29
+ * spawn — the one place to find git's registration of a worktree whose directory is gone. Absent
30
+ * on a registered session and on records from before it was recorded. */
28
31
  worktree?: {
29
32
  root: string;
30
33
  branch?: string;
34
+ primaryRoot?: string;
31
35
  } | null;
32
36
  /** Where this session lives, tagged with its multiplexer so `prune` runs the right liveness check.
33
37
  * `null` for a session in no pane and for a standing record. `window`/`session` are tmux-only. */
@@ -220,9 +224,10 @@ export declare function resolveAgent(store: Store, ref: string): AgentRecord;
220
224
  export declare function bumpLastSeen(ctx: IdContext, id: string): void;
221
225
  /** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
222
226
  export declare function touch(ctx: IdContext): void;
223
- /** Mark agents whose pane is gone or whose last-seen is stale as exited. Reconcile-culls against the
224
- * current mux's live set first, then falls through to the per-record paneExists + staleness check
225
- * (covers the other mux and sessions outside any multiplexer pane). */
227
+ /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
228
+ * first, then judges each remaining record (covers the other mux and sessions outside any
229
+ * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
230
+ * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
226
231
  export declare function prune(ctx: IdContext): AgentRecord[];
227
232
  //#endregion
228
233
  //#region src/agentdef/resolve.d.ts
@@ -328,7 +333,7 @@ interface DecommissionResult {
328
333
  export declare function decommission(ctx: IdContext, input: DecommissionInput): DecommissionResult;
329
334
  //#endregion
330
335
  //#region src/runtime/inject-inbox.d.ts
331
- type HookEvent = 'SessionStart' | 'PostToolUse';
336
+ type HookEvent = 'SessionStart';
332
337
  interface InjectPayload {
333
338
  hookSpecificOutput: {
334
339
  hookEventName: HookEvent;
@@ -347,10 +352,11 @@ interface InjectPayload {
347
352
  export declare function injectInbox(ctx: IdContext, event: string): InjectPayload | null;
348
353
  //#endregion
349
354
  //#region src/install.d.ts
355
+ type RetiredEvent = 'PostToolUse';
350
356
  type InstallStatus = 'registered' | 'already present' | 'provided by plugin' | 'removed project hook';
351
357
  interface InstallResult {
352
358
  harness: Harness;
353
- event: HookEvent;
359
+ event: HookEvent | RetiredEvent;
354
360
  vendorEvent: string;
355
361
  file: string;
356
362
  status: InstallStatus;
package/dist/index.mjs CHANGED
@@ -462,6 +462,7 @@ function reconcile(ctx, opts) {
462
462
  const cur = storablePane(ctx.env ?? process.env);
463
463
  if (!cur) return [];
464
464
  const panes = PANE_ADAPTERS[cur.mux].listPanes(exec);
465
+ if (panes.length === 0) return [];
465
466
  const live = new Set(panes.map((p) => p.id));
466
467
  const changed = [];
467
468
  for (const rec of listAgents(ctx.store)) {
@@ -477,18 +478,34 @@ function reconcile(ctx, opts) {
477
478
  if (opts?.adopt) changed.push(...adopt(ctx, panes));
478
479
  return changed;
479
480
  }
480
- /** Mark agents whose pane is gone or whose last-seen is stale as exited. Reconcile-culls against the
481
- * current mux's live set first, then falls through to the per-record paneExists + staleness check
482
- * (covers the other mux and sessions outside any multiplexer pane). */
481
+ /**
482
+ * Whether a record's pane is positively gone: its existence probe fails AND its mux answers with a
483
+ * pane list that does not hold it. `paneExists` alone collapses "unreachable" into "gone", so a mux
484
+ * that answers no list at all (server down, a failed query) leaves the pane undecided — never gone.
485
+ * The list is read at most once per mux per call.
486
+ */
487
+ function paneGone(exec, pane, lists) {
488
+ const adapter = PANE_ADAPTERS[pane.mux];
489
+ if (adapter.paneExists(exec, { id: pane.id })) return false;
490
+ let panes = lists.get(pane.mux);
491
+ if (!panes) {
492
+ panes = adapter.listPanes(exec);
493
+ lists.set(pane.mux, panes);
494
+ }
495
+ return panes.length > 0 && !panes.some((p) => p.id === pane.id);
496
+ }
497
+ /** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
498
+ * first, then judges each remaining record (covers the other mux and sessions outside any
499
+ * multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
500
+ * whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
483
501
  function prune(ctx) {
484
502
  const exec = ctx.exec ?? realExec;
485
503
  const now = ctx.now?.() ?? Date.now();
486
504
  const changed = reconcile(ctx);
505
+ const lists = /* @__PURE__ */ new Map();
487
506
  for (const rec of listAgents(ctx.store)) {
488
507
  if (!isPrunable(rec)) continue;
489
- const paneGone = rec.pane ? !PANE_ADAPTERS[rec.pane.mux].paneExists(exec, { id: rec.pane.id }) : false;
490
- const stale = now - new Date(rec.lastSeen).getTime() > STALE_MS;
491
- if (paneGone || stale) {
508
+ if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
492
509
  rec.status = "exited";
493
510
  saveAgent(ctx.store, rec);
494
511
  changed.push(rec);
@@ -1099,7 +1116,10 @@ function spawn(ctx, input) {
1099
1116
  assertDistinctFromPrimary$1(opened.worktree.root, primaryRoot);
1100
1117
  ensureMarker(join(opened.worktree.root, ".agents", "cyberlegion"));
1101
1118
  cwd = opened.worktree.root;
1102
- worktree = opened.worktree;
1119
+ worktree = {
1120
+ ...opened.worktree,
1121
+ primaryRoot
1122
+ };
1103
1123
  target = opened.target;
1104
1124
  } else {
1105
1125
  const added = gitWorktreeAdapter$1.add(exec, {
@@ -1110,7 +1130,10 @@ function spawn(ctx, input) {
1110
1130
  assertDistinctFromPrimary$1(added.root, primaryRoot);
1111
1131
  ensureMarker(join(added.root, ".agents", "cyberlegion"));
1112
1132
  cwd = added.root;
1113
- worktree = added;
1133
+ worktree = {
1134
+ ...added,
1135
+ primaryRoot
1136
+ };
1114
1137
  target = sessionAdapter.open(exec, {
1115
1138
  cwd,
1116
1139
  launch: launchLine(),
@@ -1577,12 +1600,18 @@ function decommission(ctx, input) {
1577
1600
  const exec = ctx.exec ?? realExec;
1578
1601
  const env = ctx.env ?? process.env;
1579
1602
  const worktreeRoot = rec.worktree?.root;
1580
- const primaryRoot = worktreeRoot ? resolvePrimaryRoot$1(exec) : void 0;
1581
- if (worktreeRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
1582
1603
  const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
1604
+ const primaryRoot = worktreeExists ? primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
1605
+ if (worktreeRoot && primaryRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
1583
1606
  const removesWorktree = worktreeExists && !input.keepWorktree;
1584
1607
  if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
1585
1608
  const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
1609
+ if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
1610
+ "-C",
1611
+ primaryRoot,
1612
+ "worktree",
1613
+ "prune"
1614
+ ]);
1586
1615
  if (removesWorktree) try {
1587
1616
  gitWorktreeAdapter$1.remove(exec, worktreeRoot, { primaryRoot });
1588
1617
  } catch (err) {
@@ -1603,6 +1632,22 @@ function decommission(ctx, input) {
1603
1632
  };
1604
1633
  }
1605
1634
  /**
1635
+ * The primary checkout of the repository `worktreeRoot` belongs to — `--git-common-dir` asked of the
1636
+ * worktree itself, so the answer does not depend on where the caller runs. Throws for a directory git
1637
+ * does not know: there is no repository to remove it from.
1638
+ */
1639
+ function primaryRootOf(exec, worktreeRoot) {
1640
+ const commonDir = exec("git", [
1641
+ "-C",
1642
+ worktreeRoot,
1643
+ "rev-parse",
1644
+ "--path-format=absolute",
1645
+ "--git-common-dir"
1646
+ ]);
1647
+ if (!commonDir) throw new Error(`cannot resolve the repository of worktree ${worktreeRoot} — git does not know it`);
1648
+ return dirname(commonDir);
1649
+ }
1650
+ /**
1606
1651
  * Whether the worktree holds work a removal would discard. The marker `spawn` stamps into every
1607
1652
  * worktree it creates is cyberlegion's own file, not the unit's work: in a project that does not
1608
1653
  * track it, it shows as untracked in every spawned worktree, and counting it would make a plain close
@@ -1637,10 +1682,7 @@ const VENDORS = {
1637
1682
  claude: {
1638
1683
  file: ".claude/settings.json",
1639
1684
  shape: "claude",
1640
- events: {
1641
- SessionStart: "SessionStart",
1642
- PostToolUse: "PostToolUse"
1643
- },
1685
+ events: { SessionStart: "SessionStart" },
1644
1686
  pluginHook: true
1645
1687
  },
1646
1688
  cursor: {
@@ -1652,13 +1694,11 @@ const VENDORS = {
1652
1694
  codex: {
1653
1695
  file: ".codex/hooks.json",
1654
1696
  shape: "cursor",
1655
- events: {
1656
- SessionStart: "SessionStart",
1657
- PostToolUse: "PostToolUse"
1658
- },
1697
+ events: { SessionStart: "SessionStart" },
1659
1698
  pluginHook: true
1660
1699
  }
1661
1700
  };
1701
+ const RETIRED_EVENTS = ["PostToolUse"];
1662
1702
  function readJson(file) {
1663
1703
  if (!existsSync(file)) return {};
1664
1704
  try {
@@ -1702,6 +1742,17 @@ function install(harness, projectDir = process.cwd(), pin) {
1702
1742
  status
1703
1743
  });
1704
1744
  }
1745
+ if (spec.pluginHook) for (const event of RETIRED_EVENTS) {
1746
+ if (!(spec.shape === "claude" ? removeClaude(settings, event, event) : removeCursor(settings, event, event))) continue;
1747
+ changed = true;
1748
+ results.push({
1749
+ harness,
1750
+ event,
1751
+ vendorEvent: event,
1752
+ file,
1753
+ status: "removed project hook"
1754
+ });
1755
+ }
1705
1756
  if (changed) writeJson$1(file, settings);
1706
1757
  return results;
1707
1758
  }
@@ -1716,12 +1767,10 @@ function upsertClaude(settings, event, command) {
1716
1767
  return "already present";
1717
1768
  }
1718
1769
  }
1719
- const group = { hooks: [{
1770
+ groups.push({ hooks: [{
1720
1771
  type: "command",
1721
1772
  command
1722
- }] };
1723
- if (event === "PostToolUse") group.matcher = "Write|Edit";
1724
- groups.push(group);
1773
+ }] });
1725
1774
  return "registered";
1726
1775
  }
1727
1776
  function upsertCursor(settings, event, command) {
@@ -1925,7 +1974,7 @@ function byPath(ctx, dir) {
1925
1974
  }
1926
1975
  //#endregion
1927
1976
  //#region src/runtime/inject-inbox.ts
1928
- const EVENTS = ["SessionStart", "PostToolUse"];
1977
+ const EVENTS = ["SessionStart"];
1929
1978
  /**
1930
1979
  * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
1931
1980
  * the hub's main pane), and return the SessionStart-style injection payload — or null when there is
@@ -1973,9 +2022,6 @@ function injectInbox(ctx, event) {
1973
2022
  }
1974
2023
  }
1975
2024
  } catch {}
1976
- if (rec && !rec.spawnedBy) try {
1977
- if (cur ? !ctx.store.getMainPane() : !listAgents(ctx.store).some((a) => a.kind === "standing")) parts.push("## Legion setup\n\nThis pane has no owner inbox bound yet — run `cyberlegion init` to register the surfacing hook and bind this pane as the owner live presence.");
1978
- } catch {}
1979
2025
  if (parts.length === 0) return null;
1980
2026
  return { hookSpecificOutput: {
1981
2027
  hookEventName: event,
package/hooks/hooks.json CHANGED
@@ -9,17 +9,6 @@
9
9
  }
10
10
  ]
11
11
  }
12
- ],
13
- "PostToolUse": [
14
- {
15
- "matcher": "Write|Edit",
16
- "hooks": [
17
- {
18
- "type": "command",
19
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/cyberlegion.mjs\" mail hook --event PostToolUse"
20
- }
21
- ]
22
- }
23
12
  ]
24
13
  }
25
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberlegion",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Harness-agnostic agent session spawning and messaging over the filesystem",
5
5
  "keywords": [
6
6
  "agent",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "cyberlegion",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
6
6
  "author": {
7
7
  "name": "unional"
@@ -17,7 +17,7 @@ satisfies verdict schema `V`.
17
17
  > **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
18
18
  > this plugin ships. The path is relative to this skill's own directory, not the working directory.
19
19
  > If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
20
- > with the same arguments: `npx -y cyberlegion@1.1.0`.
20
+ > with the same arguments: `npx -y cyberlegion@1.2.0`.
21
21
 
22
22
  ## 1. Resolve the agent definition
23
23
 
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {
@@ -13,7 +13,7 @@ logic, loads no other governance, and writes no state of its own.
13
13
  > **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
14
14
  > this plugin ships. The path is relative to this skill's own directory, not the working directory.
15
15
  > If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
16
- > with the same arguments: `npx -y cyberlegion@1.1.0`.
16
+ > with the same arguments: `npx -y cyberlegion@1.2.0`.
17
17
 
18
18
  ## Doorbell vs mailbox
19
19
 
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {
@@ -13,7 +13,7 @@ the one owner mailbox from wherever they are.
13
13
  > **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
14
14
  > this plugin ships. The path is relative to this skill's own directory, not the working directory.
15
15
  > If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
16
- > with the same arguments: `npx -y cyberlegion@1.1.0`.
16
+ > with the same arguments: `npx -y cyberlegion@1.2.0`.
17
17
 
18
18
  ## Resolve the owner handle
19
19
 
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {
@@ -15,7 +15,7 @@ agent (`headless-legate`, `sdd-automaton`, the cold judges) to know how *it* rep
15
15
  > **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
16
16
  > this plugin ships. The path is relative to this skill's own directory, not the working directory.
17
17
  > If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
18
- > with the same arguments: `npx -y cyberlegion@1.1.0`.
18
+ > with the same arguments: `npx -y cyberlegion@1.2.0`.
19
19
 
20
20
  ## The one probe: who, if anyone, collects my return?
21
21
 
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {
@@ -14,7 +14,7 @@ the unit's **role** — see *Mid-turn messaging is scoped by role*, below.
14
14
  > **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
15
15
  > this plugin ships. The path is relative to this skill's own directory, not the working directory.
16
16
  > If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
17
- > with the same arguments: `npx -y cyberlegion@1.1.0`.
17
+ > with the same arguments: `npx -y cyberlegion@1.2.0`.
18
18
 
19
19
  ## 1. Resolve the agent def
20
20
 
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
11
11
  import { existsSync } from 'node:fs'
12
12
  import { fileURLToPath } from 'node:url'
13
13
 
14
- const FALLBACK = 'npx -y cyberlegion@1.1.0'
14
+ const FALLBACK = 'npx -y cyberlegion@1.2.0'
15
15
  const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
16
16
 
17
17
  if (existsSync(fileURLToPath(bin))) {