cyberlegion 0.1.0 → 0.3.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.d.mts CHANGED
@@ -1,3 +1,6 @@
1
+ import { Exec, Exec as Exec$1, MuxAdapter, MuxPlacement, MuxProbe, NudgeOptions, probeMultiplexer } from "cyber-mux";
2
+ import { assertDistinctFromPrimary, gitWorktreeAdapter, resolvePrimaryRoot } from "cyber-mux/worktree";
3
+
1
4
  //#region src/store/store.d.ts
2
5
  interface Message {
3
6
  id: string;
@@ -12,7 +15,10 @@ interface Message {
12
15
  sentAt: string;
13
16
  }
14
17
  type Harness = 'claude' | 'cursor' | 'codex';
15
- type AgentStatus = 'spawning' | 'active' | 'idle' | 'stale' | 'exited' | 'paused';
18
+ /** The statuses this version writes. `spawning` is retired: spawn registers a peer `active`
19
+ * outright, since nothing flips it any more (the SessionStart hook injects no brief and mutates no
20
+ * status — see `mail/surface`). */
21
+ type AgentStatus = 'active' | 'idle' | 'stale' | 'exited' | 'paused';
16
22
  interface AgentRecord {
17
23
  id: string;
18
24
  handle: string;
@@ -23,26 +29,47 @@ interface AgentRecord {
23
29
  root: string;
24
30
  branch?: string;
25
31
  } | null;
26
- tmux?: {
27
- pane: string;
32
+ /** Where this session lives, tagged with its multiplexer so `prune` runs the right liveness check.
33
+ * `null` for a session in no pane and for a standing record. `window`/`session` are tmux-only. */
34
+ pane?: {
35
+ mux: 'tmux' | 'herdr';
36
+ id: string;
28
37
  window?: string;
29
38
  session?: string;
30
39
  } | null;
31
40
  pid?: number;
32
- status: AgentStatus;
41
+ /** A record migrated from an older hub (`admin migrate`) may carry a status this version no
42
+ * longer writes — the retired `spawning` is the known case. **Reads preserve it verbatim**: no
43
+ * read path validates, coerces, or normalizes a status, and none may start doing so
44
+ * (`mail/surface` freezes that a legacy `spawning` record keeps the status it was migrated with).
45
+ * A write that merely round-trips a record preserves it too.
46
+ *
47
+ * The exceptions are deliberate lifecycle writes, not normalizations: `register` asserts a session
48
+ * is live *now* and so writes `active`, while `prune`/`reconcile` write `exited` over whatever a
49
+ * record carried (`identity.ts`). A legacy record therefore does move off `spawning` — by an
50
+ * explicit lifecycle act, never by being read.
51
+ *
52
+ * The open member keeps the guarantee typed rather than accidental. */
53
+ status: AgentStatus | (string & {});
33
54
  createdAt: string;
34
55
  lastSeen: string;
35
56
  brief?: string;
36
57
  spawnedBy?: string;
37
58
  /** Absent ⇒ session (backward compat, no migration). 'standing' = a session-independent,
38
- * prune-exempt owner inbox minted by `identity owner`. */
59
+ * prune-exempt owner inbox minted by `unit register --standing`. */
39
60
  kind?: 'session' | 'standing';
61
+ /** Only meaningful on a `kind: 'standing'` record — the id of the unit currently standing in for
62
+ * it (`unit claim`), a per-record singleton pointer that moves as the principal moves between
63
+ * units (last claim wins). Resolved live, never trusted: a presence unit that has exited reads as
64
+ * no presence bound, same as if none were ever claimed — nothing self-heals a stale pointer, it
65
+ * stays inert until re-claimed. Independent of the hub's `mainPane` (the human's read-pane). */
66
+ presence?: string;
40
67
  }
41
68
  interface InboxSnapshot {
42
69
  unread: Message[];
43
70
  read: Message[];
44
71
  }
45
- /** The seam all mailbox + registry + brief/result access goes through. */
72
+ /** The seam all mailbox + registry + brief access goes through. */
46
73
  interface Store {
47
74
  /** This store's root (for path-relative concerns the Store itself doesn't cover, e.g. spawning
48
75
  * a worktree under a project-local root distinct from this hub). */
@@ -71,22 +98,19 @@ interface Store {
71
98
  removePaneIndex(pane: string): void;
72
99
  writeBrief(agentId: string, text: string): void;
73
100
  readBrief(agentId: string): string | undefined;
74
- /** Path this dispatch's result file would live at (computed only — no IO; existence not implied). */
75
- resultPath(id: string): string;
76
- /** Write a dispatch's result as JSON text. */
77
- writeResult(id: string, value: unknown): void;
78
- /** Read a dispatch's result JSON text, or undefined if not yet written. */
79
- readResult(id: string): string | undefined;
101
+ /** Set (or move) the hub's single main pane — the standing owner's live presence — or clear it
102
+ * (`null`, a no-op when already unbound). A hub-level singleton, independent of any agent record. */
103
+ setMainPane(pane: string | null): void;
104
+ /** The hub's currently bound main pane, or undefined when none is bound. */
105
+ getMainPane(): string | undefined;
80
106
  }
81
107
  //#endregion
82
108
  //#region src/identity.d.ts
83
- /** Runs a command synchronously; returns trimmed stdout, or null on any failure. */
84
- type Exec = (cmd: string, args: string[]) => string | null;
85
- declare const realExec: Exec;
109
+ declare const realExec: Exec$1;
86
110
  interface IdContext {
87
111
  store: Store;
88
112
  env?: NodeJS.ProcessEnv;
89
- exec?: Exec;
113
+ exec?: Exec$1;
90
114
  now?: () => number;
91
115
  }
92
116
  declare function loadAgent(store: Store, id: string): AgentRecord | undefined;
@@ -94,10 +118,11 @@ declare function listAgents(store: Store): AgentRecord[];
94
118
  /** Detect the harness: explicit wins, then env probes, then the tmux pane command. */
95
119
  declare function detectHarness(explicit: string | undefined, ctx: IdContext): Harness | undefined;
96
120
  /**
97
- * Recover the calling agent's own id. In tmux the pane index is authoritative — a pane with no
98
- * pane entry is simply unregistered and must NOT adopt `$CYBERLEGION_AGENT_ID` (that env fallback
99
- * applies only when there is no `$TMUX_PANE`). There is no shared bare "self" file — self-id is
100
- * always pane-keyed or explicit via the env var.
121
+ * Recover the calling agent's own id, mux-agnostically. Inside any multiplexer pane (tmux or herdr)
122
+ * the pane index is authoritative — a pane with no pane entry is simply unregistered and must NOT
123
+ * adopt `$CYBERLEGION_AGENT_ID` (that env fallback applies ONLY when the session is in no
124
+ * multiplexer pane at all). There is no shared bare "self" file — self-id is always pane-keyed or
125
+ * explicit via the env var.
101
126
  */
102
127
  declare function resolveSelfId(ctx: IdContext): string | undefined;
103
128
  interface RegisterInput {
@@ -106,7 +131,8 @@ interface RegisterInput {
106
131
  }
107
132
  /** Register (or idempotently refresh) this session's identity. */
108
133
  declare function register(ctx: IdContext, input: RegisterInput): AgentRecord;
109
- /** Resolve a recipient argument (id or handle) to an agent id. */
134
+ /** Resolve a recipient argument (id or handle) to an agent id. A handle resolves to live units
135
+ * only — mail addressed to an exited unit lands in an inbox nobody reads. */
110
136
  declare function resolveRecipient(store: Store, to: string): string;
111
137
  /**
112
138
  * Resolve a unit reference by id, handle, or its worktree branch (the unit↔CR join key: an
@@ -117,7 +143,9 @@ declare function resolveAgent(store: Store, ref: string): AgentRecord;
117
143
  declare function bumpLastSeen(ctx: IdContext, id: string): void;
118
144
  /** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
119
145
  declare function touch(ctx: IdContext): void;
120
- /** Mark agents whose pane is gone or whose last-seen is stale as exited. */
146
+ /** Mark agents whose pane is gone or whose last-seen is stale as exited. Reconcile-culls against the
147
+ * current mux's live set first, then falls through to the per-record paneExists + staleness check
148
+ * (covers the other mux and sessions outside any multiplexer pane). */
121
149
  declare function prune(ctx: IdContext): AgentRecord[];
122
150
  //#endregion
123
151
  //#region src/agentdef/resolve.d.ts
@@ -168,143 +196,6 @@ interface RealizedLaunch {
168
196
  * `--model` when known, `--append-system-prompt` carrying the def's instructions body) since the
169
197
  * exact per-harness flag surface is realized later by the launching caller, not by cyberlegion. */
170
198
  declare function realizeLaunch(def: AgentDef, opts?: RealizeLaunchOptions): RealizedLaunch;
171
- interface RealizeSubagentOptions {
172
- /** File the spawned subagent reads its brief from. */
173
- briefFile: string;
174
- /** File the spawned subagent writes its result JSON to. */
175
- resultFile: string;
176
- }
177
- /** Build the instruction string a parent model uses to spawn its OWN harness Task subagent for
178
- * `def` — cyberlegion never invokes the Task tool itself. Names `subagent_type: <def.name>` for a
179
- * harness that recognizes it, but always inlines the model + full instructions too so the same
180
- * instruction string is correct even when the harness has no such named subagent type. */
181
- declare function realizeSubagentInstruction(def: AgentDef, opts: RealizeSubagentOptions): string;
182
- //#endregion
183
- //#region src/console/worktree.d.ts
184
- /** Generic multiplexer/worktree seam — no legion-specific concepts. */
185
- interface WorktreeAddOptions {
186
- /** The primary checkout's root — the repo `git worktree add` runs against. */
187
- primaryRoot: string;
188
- /** Where the new worktree should be checked out. */
189
- path: string;
190
- /** Branch to create the worktree on. */
191
- branch: string;
192
- }
193
- interface Worktree {
194
- root: string;
195
- branch: string;
196
- }
197
- interface WorktreeRemoveOptions {
198
- primaryRoot: string;
199
- }
200
- interface WorktreeAdapter {
201
- add(exec: Exec, opts: WorktreeAddOptions): Worktree;
202
- remove(exec: Exec, path: string, opts: WorktreeRemoveOptions): void;
203
- }
204
- /** The only worktree backend at MVP — plain `git worktree`. */
205
- declare const gitWorktreeAdapter: WorktreeAdapter;
206
- /**
207
- * Resolve the primary checkout's root regardless of whether the caller's cwd is the primary
208
- * checkout or a linked worktree — `--git-common-dir` always points at the main repo's `.git`.
209
- */
210
- declare function resolvePrimaryRoot(exec: Exec): string;
211
- /**
212
- * Refuse the primary checkout: a spawned unit's resolved worktree root must never be the primary
213
- * checkout itself. A unit is never run in the primary checkout.
214
- */
215
- declare function assertDistinctFromPrimary(worktreeRoot: string, primaryRoot: string): void;
216
- //#endregion
217
- //#region src/console/session.d.ts
218
- /** Generic multiplexer seam — no legion-specific concepts, so this composes with any host. */
219
- /** Where a new pane/window/session should be placed relative to the caller's current one.
220
- * `'workspace'` opens a genuinely separate workspace/session (herdr: `workspace create`; tmux: a
221
- * new detached session) — the caller's current workspace/session is left untouched, unlike every
222
- * other placement, which adds a pane/window inside it. */
223
- type SessionPlacement = 'pane:right' | 'pane:down' | 'tab' | 'window' | 'workspace';
224
- interface SessionOpenOptions {
225
- /** Working directory the new pane/window/session should start in. */
226
- cwd: string;
227
- /** Command line to launch inside the new pane once it is open. */
228
- launch: string;
229
- /** Placement relative to the caller; defaults to 'pane:right'. */
230
- at?: SessionPlacement;
231
- }
232
- /** Opaque handle to an open pane/window/session; backend-specific id lives in `id`. */
233
- interface SessionTarget {
234
- id: string;
235
- }
236
- interface SessionReadOptions {
237
- /** How many trailing lines of output to capture; omit for the backend's default. */
238
- lines?: number;
239
- }
240
- interface OpenInNewWorktreeOptions {
241
- /** The primary checkout's root — the repo the new worktree branches from. */
242
- primaryRoot: string;
243
- /** Branch to create the worktree on. */
244
- branch: string;
245
- /** Where the new worktree should be checked out. */
246
- path: string;
247
- /** Command line to launch inside the new session once it is open. */
248
- launch: string;
249
- }
250
- interface SessionAdapter {
251
- /** Backend name, e.g. "tmux" or "herdr". */
252
- readonly name: string;
253
- /** Create a new pane/window running `opts.launch` in `opts.cwd`; returns its target handle. */
254
- open(exec: Exec, opts: SessionOpenOptions): SessionTarget;
255
- /**
256
- * Atomically create a new worktree AND open it in a genuinely new workspace/session, when the
257
- * backend has a native primitive for that combination (herdr's `worktree create` both creates
258
- * the worktree and nests it under its source workspace in one call — a real organizational
259
- * improvement over a separate worktree-add followed by a plain `open()`). Backends without such
260
- * a primitive (tmux has none) omit this; callers fall back to creating the worktree themselves
261
- * and then calling `open()` with `at: 'workspace'`.
262
- */
263
- openInNewWorktree?(exec: Exec, opts: OpenInNewWorktreeOptions): {
264
- target: SessionTarget;
265
- worktree: Worktree;
266
- };
267
- /** Type text into the target session (submitted, not queued). */
268
- send(exec: Exec, target: SessionTarget, text: string): void;
269
- /** Capture the target session's current output. */
270
- read(exec: Exec, target: SessionTarget, opts?: SessionReadOptions): string;
271
- /** Move input focus to the target session; best-effort (may no-op if the backend can't). */
272
- focus(exec: Exec, target: SessionTarget): void;
273
- /** Close the target session. */
274
- teardown(exec: Exec, target: SessionTarget): void;
275
- }
276
- //#endregion
277
- //#region src/console/index.d.ts
278
- /**
279
- * Backend selection via the two-mode mux probe (`$CYBERLEGION_MUX` fast-path/override, else
280
- * ancestry discovery from `$$` falling back to the `$TMUX`/`$HERDR_ENV` hint when the walk is
281
- * inconclusive) — tmux/herdr map to their existing adapters; anything else is a clear error.
282
- */
283
- declare function selectSessionAdapter(env: NodeJS.ProcessEnv, exec?: Exec): SessionAdapter;
284
- //#endregion
285
- //#region src/console/mux-probe.d.ts
286
- type Mux = 'tmux' | 'herdr' | 'screen' | 'none';
287
- interface MuxProbe {
288
- mux: Mux;
289
- pane?: string;
290
- /** 'env' — trusted the $CYBERLEGION_MUX fast-path/override. 'ancestry' — walked the process tree. */
291
- via: 'env' | 'ancestry';
292
- }
293
- /**
294
- * Two-mode multiplexer detection.
295
- *
296
- * Fast-path: `$CYBERLEGION_MUX` (tmux | herdr | screen | none) is trusted outright — this also
297
- * serves as an OVERRIDE (`=none` forces no-mux even inside a real multiplexer). `$CYBERLEGION_MUX_PANE`
298
- * carries the pane id alongside it.
299
- *
300
- * Discovery (else): walk the process ancestry from `$$` via `ps -o ppid=,comm= -p <pid>`, since the
301
- * tool's own shell may not be the human's pane. `$TMUX`/`$HERDR_ENV` are NOT trusted alone — they
302
- * are used only as a fast-positive hint the ancestry walk falls back to when the walk itself is
303
- * inconclusive (e.g. `ps` unavailable), never as a substitute for it.
304
- */
305
- declare function probeMultiplexer(exec: Exec, env: NodeJS.ProcessEnv, opts?: {
306
- discover?: boolean;
307
- }): MuxProbe;
308
199
  //#endregion
309
200
  //#region src/decommission.d.ts
310
201
  interface DecommissionInput {
@@ -326,78 +217,6 @@ interface DecommissionResult {
326
217
  */
327
218
  declare function decommission(ctx: IdContext, input: DecommissionInput): DecommissionResult;
328
219
  //#endregion
329
- //#region src/dispatch/prep.d.ts
330
- interface DispatchContext {
331
- store: Store;
332
- }
333
- interface PrepInput {
334
- /** Resolve an agent def (`.agents/agents/<name>.md`) to build the instruction from. */
335
- agent?: string;
336
- /** Read an exact agent def file instead of resolving by name (plugin-scoped escape hatch). */
337
- agentFile?: string;
338
- /** Role label folded into the generic instruction when no agent def is given. */
339
- role?: string;
340
- /** Brief body text. */
341
- briefText?: string;
342
- /** Read the brief from a file, or `-` for stdin. */
343
- briefFile?: string;
344
- /** JSON schema file the eventual result must satisfy — carried through by callers that validate. */
345
- verdictSchema?: string;
346
- /** Thread id correlating a channel-path reply; defaults to the minted dispatch id. */
347
- thread?: string;
348
- }
349
- interface DispatchEnvelope {
350
- id: string;
351
- thread: string;
352
- briefFile: string;
353
- resultFile: string;
354
- instruction: string;
355
- }
356
- /**
357
- * Allocate a dispatch: mint an id (thread defaults to it), write the brief into the Store, compute
358
- * the result-slot path, and build the instruction from a resolved agent def (or a generic one).
359
- * Spawns nothing — the envelope is handed to whatever spawns next (a Task tool, `channel()`, a
360
- * human).
361
- */
362
- declare function prep(ctx: DispatchContext, input: PrepInput, readStdin?: () => string): DispatchEnvelope;
363
- //#endregion
364
- //#region src/dispatch/channel.d.ts
365
- interface ChannelInput extends PrepInput {
366
- /** Block for the mail-thread reply and return a validated `DispatchResult`; without it, the peer
367
- * is spawned and the envelope is returned so the caller can `mail await` later. */
368
- wait?: boolean;
369
- /** Placement relative to the caller; defaults to 'pane:right'. */
370
- at?: SessionPlacement;
371
- timeoutMs?: number;
372
- maxWaitS?: number;
373
- }
374
- interface DispatchResult {
375
- id: string;
376
- verdict?: unknown;
377
- body: string;
378
- ts: number;
379
- }
380
- /** Thrown by a `--wait` channel call when its internal poll-cycle cap is hit with no reply yet — a
381
- * clean, re-armable sentinel (mirrors `mail await`'s "waiting" outcome), never a failure. */
382
- declare class DispatchWaitingError extends Error {
383
- readonly thread: string;
384
- constructor(thread: string);
385
- }
386
- /**
387
- * `prep()` + spawn a peer session realized from the same agent def + (optionally) `awaitReply` on
388
- * the thread. Requires `--agent`/`--agent-file` (a channel peer needs a real launch to realize).
389
- */
390
- declare function channel(ctx: IdContext, input: ChannelInput, readStdin?: () => string): Promise<DispatchResult | DispatchEnvelope>;
391
- //#endregion
392
- //#region src/dispatch/collect.d.ts
393
- interface CollectContext {
394
- store: Store;
395
- now?: () => number;
396
- }
397
- /** Read + validate a dispatch's result file. Throws (fail-loud) when the file is not yet written,
398
- * or when it fails the optional verdict schema. */
399
- declare function collect(ctx: CollectContext, id: string, verdictSchema?: string): DispatchResult;
400
- //#endregion
401
220
  //#region src/runtime/inject-inbox.d.ts
402
221
  type HookEvent = 'SessionStart' | 'PostToolUse';
403
222
  interface InjectPayload {
@@ -407,9 +226,13 @@ interface InjectPayload {
407
226
  };
408
227
  }
409
228
  /**
410
- * Resolve the calling agent, gather its pending brief + unread mail, and return the
411
- * SessionStart-style injection payload — or null when there is nothing to inject (an
412
- * unregistered caller or an empty inbox never fails the harness hook).
229
+ * Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
230
+ * the hub's main pane), and return the SessionStart-style injection payload — or null when there is
231
+ * nothing to inject (an unregistered caller or an empty inbox never fails the harness hook).
232
+ *
233
+ * This never injects a brief. Brief delivery is the spawn wake's job: the first-turn doorbell
234
+ * carries the instruction and names the brief's file path, so pickup does not depend on this hook
235
+ * firing in the child (`unit/lifecycle`, superseding ADR-0027).
413
236
  */
414
237
  declare function injectInbox(ctx: IdContext, event: string): InjectPayload | null;
415
238
  //#endregion
@@ -423,7 +246,7 @@ interface InstallResult {
423
246
  status: InstallStatus;
424
247
  }
425
248
  /** Register the surfacing hook into one harness's config, idempotently. */
426
- declare function install(harness: Harness, projectDir?: string): InstallResult[];
249
+ declare function install(harness: Harness, projectDir?: string, pin?: string): InstallResult[];
427
250
  //#endregion
428
251
  //#region src/message.d.ts
429
252
  interface MsgContext {
@@ -461,6 +284,21 @@ declare function ack(ctx: MsgContext, meId: string, msgId: string): Message;
461
284
  /** Permanently remove a message (unread or already-acked) from the caller's inbox. */
462
285
  declare function deleteMessage(ctx: MsgContext, meId: string, msgId: string): void;
463
286
  //#endregion
287
+ //#region src/mux-select.d.ts
288
+ /**
289
+ * Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
290
+ * `$CYBERLEGION_MUX*` → `$CYBER_MUX*` env seam (`mux-env.ts`) — tmux/herdr map to their existing
291
+ * cyber-mux adapters.
292
+ *
293
+ * cyber-mux detects MORE backends than `unit/registry`'s `AgentRecord.pane` can carry a locator
294
+ * under (`'tmux' | 'herdr'` only). A DETECTED wezterm/zellij is refused HERE, before anything opens,
295
+ * naming the backend it found (mux.feature: "a detected backend a unit record cannot carry is
296
+ * refused before opening anything") — driving it would open a real pane no record could name,
297
+ * stranding a live session `prune` can never reap and no caller can nudge. Anything else (`none`,
298
+ * `screen`) falls through to the plain "no backend" refusal, unchanged from before the migration.
299
+ */
300
+ declare function selectSessionAdapter(env: NodeJS.ProcessEnv, exec?: Exec): MuxAdapter;
301
+ //#endregion
464
302
  //#region src/output.d.ts
465
303
  type Format = 'toon' | 'json';
466
304
  /** One TOON-encoded object: `key: value` lines, blank/undefined fields dropped. */
@@ -488,13 +326,13 @@ declare function fail(msg: string): never;
488
326
  /** Walk up from `cwd` to the nearest git repo root; fall back to `cwd`. */
489
327
  declare function projectRoot(cwd?: string): string;
490
328
  /** Runs a command synchronously in `cwd`; returns trimmed stdout, or null on any failure. */
491
- type Exec$1 = (cmd: string, args: string[]) => string | null;
329
+ type Exec$2 = (cmd: string, args: string[]) => string | null;
492
330
  interface RootOptions {
493
331
  root?: string;
494
332
  space?: string;
495
333
  cwd?: string;
496
334
  env?: NodeJS.ProcessEnv;
497
- exec?: Exec$1;
335
+ exec?: Exec$2;
498
336
  }
499
337
  /**
500
338
  * Resolve the cyberlegion hub root. Precedence: explicit --root/--space, then `$CYBERLEGION_ROOT`,
@@ -525,7 +363,7 @@ declare const paths: {
525
363
  inboxReadDir: (root: string, id: string) => string;
526
364
  dataDir: (root: string, id: string) => string;
527
365
  briefFile: (root: string, id: string) => string;
528
- resultFile: (root: string, id: string) => string;
366
+ mainPaneFile: (root: string) => string;
529
367
  };
530
368
  //#endregion
531
369
  //#region src/session.d.ts
@@ -548,8 +386,8 @@ interface SpawnInput {
548
386
  /** Spawn into this existing directory instead — creates no worktree. Mutually exclusive with
549
387
  * `branch`/`worktreePath` (those create a worktree; this reuses one). */
550
388
  cwd?: string;
551
- /** Placement relative to the caller; defaults to 'pane:right'. */
552
- at?: SessionPlacement;
389
+ /** Placement relative to the caller; defaults to 'tab'. */
390
+ at?: MuxPlacement;
553
391
  }
554
392
  interface SpawnResult {
555
393
  agent: AgentRecord;
@@ -559,8 +397,10 @@ interface SpawnResult {
559
397
  /**
560
398
  * Launch a new peer session as a genuine sibling unit: create a real git worktree distinct from
561
399
  * the primary checkout (refuse the primary checkout), open a session backend (tmux or herdr) with
562
- * its cwd set to that worktree, pre-register the peer, and drop its brief as a file the peer's own
563
- * SessionStart hook reads — never typed into its prompt.
400
+ * its cwd set to that worktree, pre-register the peer, and drop its brief as a file — never typed
401
+ * into its prompt. Nothing loads that file for the peer: it is the spawn wake that tells the peer to
402
+ * go read it, naming its path (`spawnAndWake` below, and ADR-0032). `spawn` alone therefore leaves a
403
+ * peer sitting idle with its brief unread, which is why callers want `spawnAndWake`.
564
404
  *
565
405
  * Spawned unit worktrees live sibling to the primary checkout (`<parent>/<repo>.worktrees/legion-<id6>`),
566
406
  * never nested inside it, even though the registry/mailbox itself lives in the global hub — the hub
@@ -568,6 +408,26 @@ interface SpawnResult {
568
408
  * scoped to this one project's git remote.
569
409
  */
570
410
  declare function spawn(ctx: IdContext, input: SpawnInput): SpawnResult;
411
+ /**
412
+ * `unit spawn` end to end: open the peer, then deliver its first turn.
413
+ *
414
+ * The two acts live together here rather than at the CLI call site so the **brief path the doorbell
415
+ * names is derived from the record `spawn` just wrote**, not handed in by a caller. That is the
416
+ * point of the seam: a caller cannot ring with the wrong path because it supplies no path — an
417
+ * empty string, a stale path, or the brief's whole body are all unrepresentable here, rather than
418
+ * merely untested.
419
+ *
420
+ * The ring is best-effort on top of the guaranteed spawn effect (worktree + session + registry
421
+ * record): `--no-wake` rings nothing, and a ring that never completes is reported as a warning on
422
+ * the result rather than thrown, so it can never fail a spawn that already landed.
423
+ */
424
+ declare function spawnAndWake(ctx: IdContext, input: SpawnInput, options?: {
425
+ noWake?: boolean;
426
+ nudgeOpts?: NudgeOptions;
427
+ }): Promise<SpawnResult & {
428
+ rung: boolean;
429
+ warning?: string;
430
+ }>;
571
431
  //#endregion
572
432
  //#region src/store/file-store.d.ts
573
433
  /** The on-disk `Store` implementation — current per-writer sharded `.json` layout (ADR-0020):
@@ -591,9 +451,8 @@ declare class FileStore implements Store {
591
451
  removePaneIndex(pane: string): void;
592
452
  writeBrief(agentId: string, text: string): void;
593
453
  readBrief(agentId: string): string | undefined;
594
- resultPath(id: string): string;
595
- writeResult(id: string, value: unknown): void;
596
- readResult(id: string): string | undefined;
454
+ setMainPane(pane: string | null): void;
455
+ getMainPane(): string | undefined;
597
456
  }
598
457
  //#endregion
599
458
  //#region src/wake/await.d.ts
@@ -630,46 +489,6 @@ type AwaitOutcome = {
630
489
  */
631
490
  declare function awaitReply(ctx: MsgContext, input: AwaitInput): Promise<AwaitOutcome>;
632
491
  //#endregion
633
- //#region src/wake/wake-path.d.ts
634
- /**
635
- * The wake-matrix path a gateway would drive a turn through. Pure decision helper — no I/O, no
636
- * polling, no session backend calls; the gateway (`legion-gateway-legate`, CR-5) composes this with
637
- * the actual `mail await` / `session nudge` primitives.
638
- *
639
- * - **A** — a single unbounded `mail await` (not returned here; superseded by A-loop everywhere the
640
- * gateway drives this decision, since a raw A risks a harness tool-timeout SIGKILL).
641
- * - **A-loop** — the portable default: a bounded `mail await` cycle, re-armed by the caller.
642
- * - **A-prime** — Claude Code with an observable background task: run `await` via
643
- * `run_in_background` and poll its result instead of blocking the turn.
644
- * - **B** — a live foreign session behind a verified multiplexer: ring the doorbell
645
- * (`session nudge`) instead of polling. Never selected without a mux.
646
- * - **C** — own-the-PTY (dropped from the shipped matrix; kept as a type for completeness).
647
- * - **D** — `/loop`-style external re-invocation (dropped from the shipped matrix).
648
- * - **E** — hook surfacing at a turn boundary (selected by the caller directly, not by this
649
- * function — it has no wait/poll shape to decide between).
650
- */
651
- type WakePath = 'A' | 'A-loop' | 'A-prime' | 'B' | 'C' | 'D' | 'E';
652
- interface WakePathInput {
653
- /** The calling harness, e.g. 'claude' | 'cursor' | 'codex'. */
654
- harness: string;
655
- mux: MuxProbe;
656
- /** The harness can observe a backgrounded task's result without blocking the turn. */
657
- observable?: boolean;
658
- /** The target is a live, foreign interactive session that can be rung rather than polled. */
659
- dedicatedListener?: boolean;
660
- }
661
- interface WakePathResult {
662
- path: WakePath;
663
- why: string;
664
- }
665
- /**
666
- * Select a wake path from the shipped matrix. Rule order: a `mux.mux === 'none'` caller can never
667
- * get **B** — it always falls through to the bounded poll. Claude Code + an observable background
668
- * task prefers **A-prime**. A live foreign session behind a verified mux prefers **B**. Otherwise
669
- * the portable default is **A-loop**.
670
- */
671
- declare function selectWakePath(input: WakePathInput): WakePathResult;
672
- //#endregion
673
492
  //#region src/wake/watch.d.ts
674
493
  interface WatchInput {
675
494
  meId: string;
@@ -688,4 +507,4 @@ interface WatchOptions {
688
507
  */
689
508
  declare function watchMail(ctx: MsgContext, input: WatchInput, onMessage: (msg: InboxItem) => void, opts?: WatchOptions): Promise<void>;
690
509
  //#endregion
691
- export { type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, type ChannelInput, type DecommissionInput, type DecommissionResult, type DispatchEnvelope, type DispatchResult, DispatchWaitingError, type Exec, FileStore, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type PrepInput, type RegisterInput, type RootOptions, type SendInput, type SpawnInput, type SpawnResult, type Store, type WakePathInput, type WakePathResult, ack, assertDistinctFromPrimary, awaitReply, bumpLastSeen, channel, collect, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, inbox, injectInbox, install, listAgentDefs, listAgents, loadAgent, nextStep, paths, peek, prep, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, realizeSubagentInstruction, register, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, selectSessionAdapter, selectWakePath, send, spawn, toonList, toonObject, touch, watchMail };
510
+ export { type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, type DecommissionInput, type DecommissionResult, type Exec, FileStore, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type RegisterInput, type RootOptions, type SendInput, type SpawnInput, type SpawnResult, type Store, ack, assertDistinctFromPrimary, awaitReply, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, inbox, injectInbox, install, listAgentDefs, listAgents, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, selectSessionAdapter, send, spawn, spawnAndWake, toonList, toonObject, touch, watchMail };
package/dist/index.mjs CHANGED
@@ -1,28 +1,4 @@
1
- import { A as detectHarness, C as resolveAgentDef, D as selectSessionAdapter, E as spawn, F as register, G as resolveProjectLocalRoot, H as ensureMarker, J as gitWorktreeAdapter, K as resolveRoot, L as resolveAgent, M as loadAgent, N as prune, O as probeMultiplexer, P as realExec, R as resolveRecipient, S as listAgentDefs, T as realizeSubagentInstruction, U as paths, V as touch, W as projectRoot, Y as resolvePrimaryRoot, _ as inbox, a as fail, b as send, c as toonObject, d as DispatchWaitingError, f as channel, g as deleteMessage, h as ack, i as emit, j as listAgents, k as bumpLastSeen, l as install, m as awaitReply, n as FileStore, o as nextStep, p as prep, q as assertDistinctFromPrimary, r as injectInbox, s as toonList, t as watchMail, u as collect, v as peek, w as realizeLaunch, x as decommission, y as resolveBody, z as resolveSelfId } from "./watch-BuYsdwhx.mjs";
2
- //#region src/wake/wake-path.ts
3
- /**
4
- * Select a wake path from the shipped matrix. Rule order: a `mux.mux === 'none'` caller can never
5
- * get **B** — it always falls through to the bounded poll. Claude Code + an observable background
6
- * task prefers **A-prime**. A live foreign session behind a verified mux prefers **B**. Otherwise
7
- * the portable default is **A-loop**.
8
- */
9
- function selectWakePath(input) {
10
- if (input.mux.mux === "none") return {
11
- path: "A-loop",
12
- why: "no multiplexer detected — bounded poll only, B is never available"
13
- };
14
- if (input.harness === "claude" && input.observable) return {
15
- path: "A-prime",
16
- why: "Claude Code + an observable background task — await via run_in_background"
17
- };
18
- if (input.dedicatedListener) return {
19
- path: "B",
20
- why: `a live foreign session behind a verified ${input.mux.mux} mux — ring the doorbell`
21
- };
22
- return {
23
- path: "A-loop",
24
- why: "portable default — bounded mail await cycle"
25
- };
26
- }
27
- //#endregion
28
- export { DispatchWaitingError, FileStore, ack, assertDistinctFromPrimary, awaitReply, bumpLastSeen, channel, collect, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, inbox, injectInbox, install, listAgentDefs, listAgents, loadAgent, nextStep, paths, peek, prep, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, realizeSubagentInstruction, register, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, selectSessionAdapter, selectWakePath, send, spawn, toonList, toonObject, touch, watchMail };
1
+ import { $ as resolveProjectLocalRoot, A as bumpLastSeen, D as selectSessionAdapter, E as spawnAndWake, F as loadAgent, I as prune, J as listAgentDefs, K as touch, L as realExec, N as detectHarness, P as listAgents, Q as projectRoot, T as spawn, U as resolveRecipient, V as resolveAgent, W as resolveSelfId, X as ensureMarker, Y as resolveAgentDef, Z as paths, _ as install, a as emit, c as toonList, d as deleteMessage, et as resolveRoot, f as inbox, g as send, h as resolveBody, i as injectInbox, l as toonObject, n as awaitReply, o as fail, p as peek, r as FileStore, s as nextStep, t as watchMail, u as ack, v as decommission, y as realizeLaunch, z as register } from "./watch-BZmEAqUo.mjs";
2
+ import { probeMultiplexer } from "cyber-mux";
3
+ import { assertDistinctFromPrimary, gitWorktreeAdapter, resolvePrimaryRoot } from "cyber-mux/worktree";
4
+ export { FileStore, ack, assertDistinctFromPrimary, awaitReply, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, inbox, injectInbox, install, listAgentDefs, listAgents, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, selectSessionAdapter, send, spawn, spawnAndWake, toonList, toonObject, touch, watchMail };