cyberlegion 0.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.
@@ -0,0 +1,656 @@
1
+ //#region src/store/store.d.ts
2
+ interface Message {
3
+ id: string;
4
+ from: string;
5
+ fromHandle: string;
6
+ to: string;
7
+ subject?: string;
8
+ body: string;
9
+ thread?: string;
10
+ replyTo?: string;
11
+ ts: number;
12
+ sentAt: string;
13
+ }
14
+ type Harness = 'claude' | 'cursor' | 'codex';
15
+ type AgentStatus = 'spawning' | 'active' | 'idle' | 'stale' | 'exited' | 'paused';
16
+ interface AgentRecord {
17
+ id: string;
18
+ handle: string;
19
+ harness: Harness;
20
+ cwd: string;
21
+ worktree?: {
22
+ root: string;
23
+ branch?: string;
24
+ } | null;
25
+ tmux?: {
26
+ pane: string;
27
+ window?: string;
28
+ session?: string;
29
+ } | null;
30
+ pid?: number;
31
+ status: AgentStatus;
32
+ createdAt: string;
33
+ lastSeen: string;
34
+ brief?: string;
35
+ spawnedBy?: string;
36
+ }
37
+ interface InboxSnapshot {
38
+ unread: Message[];
39
+ read: Message[];
40
+ }
41
+ /** The seam all mailbox + registry + brief/result access goes through. */
42
+ interface Store {
43
+ /** This store's root (for path-relative concerns the Store itself doesn't cover, e.g. spawning
44
+ * a worktree under a project-local root distinct from this hub). */
45
+ readonly root: string;
46
+ /** Ensure this store's root is initialized (tracked marker present). Idempotent. */
47
+ ensureMarker(): void;
48
+ /** Write one message into `toId`'s inbox. Collision-free by `msg.id`. */
49
+ putMessage(toId: string, msg: Message): void;
50
+ /** The full unread/read split for one agent's inbox. */
51
+ listInbox(id: string): InboxSnapshot;
52
+ /** Ack a message by moving it out of the unread set; throws if it is not currently unread. */
53
+ ackMessage(id: string, msgId: string): Message;
54
+ /** Permanently remove a message (unread or already-acked) from `id`'s inbox; throws if absent. */
55
+ removeMessage(id: string, msgId: string): void;
56
+ /** Upsert an agent record (keyed by `rec.id`). */
57
+ putAgent(rec: AgentRecord): void;
58
+ getAgent(id: string): AgentRecord | undefined;
59
+ listAgents(): AgentRecord[];
60
+ removeAgent(id: string): void;
61
+ removeAgentData(id: string): void;
62
+ putPaneIndex(pane: string, agentId: string): void;
63
+ resolvePaneId(pane: string): string | undefined;
64
+ /** Reverse-lookup: an agent's pane from the index, when the agent record itself carries none
65
+ * (e.g. a herdr peer, whose pane is stored only in this index). */
66
+ findPaneByAgentId(agentId: string): string | undefined;
67
+ removePaneIndex(pane: string): void;
68
+ writeBrief(agentId: string, text: string): void;
69
+ readBrief(agentId: string): string | undefined;
70
+ /** Path this dispatch's result file would live at (computed only — no IO; existence not implied). */
71
+ resultPath(id: string): string;
72
+ /** Write a dispatch's result as JSON text. */
73
+ writeResult(id: string, value: unknown): void;
74
+ /** Read a dispatch's result JSON text, or undefined if not yet written. */
75
+ readResult(id: string): string | undefined;
76
+ }
77
+ //#endregion
78
+ //#region src/identity.d.ts
79
+ /** Runs a command synchronously; returns trimmed stdout, or null on any failure. */
80
+ type Exec = (cmd: string, args: string[]) => string | null;
81
+ declare const realExec: Exec;
82
+ interface IdContext {
83
+ store: Store;
84
+ env?: NodeJS.ProcessEnv;
85
+ exec?: Exec;
86
+ now?: () => number;
87
+ }
88
+ declare function loadAgent(store: Store, id: string): AgentRecord | undefined;
89
+ declare function listAgents(store: Store): AgentRecord[];
90
+ /** Detect the harness: explicit wins, then env probes, then the tmux pane command. */
91
+ declare function detectHarness(explicit: string | undefined, ctx: IdContext): Harness | undefined;
92
+ /**
93
+ * Recover the calling agent's own id. In tmux the pane index is authoritative — a pane with no
94
+ * pane entry is simply unregistered and must NOT adopt `$CYBERLEGION_AGENT_ID` (that env fallback
95
+ * applies only when there is no `$TMUX_PANE`). There is no shared bare "self" file — self-id is
96
+ * always pane-keyed or explicit via the env var.
97
+ */
98
+ declare function resolveSelfId(ctx: IdContext): string | undefined;
99
+ interface RegisterInput {
100
+ handle?: string;
101
+ harness?: string;
102
+ }
103
+ /** Register (or idempotently refresh) this session's identity. */
104
+ declare function register(ctx: IdContext, input: RegisterInput): AgentRecord;
105
+ /** Resolve a recipient argument (id or handle) to an agent id. */
106
+ declare function resolveRecipient(store: Store, to: string): string;
107
+ /**
108
+ * Resolve a unit reference by id, handle, or its worktree branch (the unit↔CR join key: an
109
+ * `AgentRecord.worktree.branch` equals the SDD `<cr-ref>` it maps to, when spawned for one). Used
110
+ * by verbs that address "the unit working on CR X" as well as "the unit named X".
111
+ */
112
+ declare function resolveAgent(store: Store, ref: string): AgentRecord;
113
+ declare function bumpLastSeen(ctx: IdContext, id: string): void;
114
+ /** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
115
+ declare function touch(ctx: IdContext): void;
116
+ /** Mark agents whose pane is gone or whose last-seen is stale as exited. */
117
+ declare function prune(ctx: IdContext): AgentRecord[];
118
+ //#endregion
119
+ //#region src/agentdef/resolve.d.ts
120
+ interface AgentDef {
121
+ name: string;
122
+ description?: string;
123
+ model?: string;
124
+ effort?: string;
125
+ harness?: Harness;
126
+ warm?: boolean;
127
+ interactive?: boolean;
128
+ instructions: string;
129
+ path: string;
130
+ }
131
+ interface ResolveAgentDefInput {
132
+ /** Resolve `<name>.md` by search — mutually exclusive with `file`. */
133
+ name?: string;
134
+ /** Read this exact path — bypasses name search entirely (the plugin-scoped escape hatch). */
135
+ file?: string;
136
+ /** Extra dirs to search (each dir is checked for `<name>.md`), ahead of the project convention. */
137
+ searchRoots?: string[];
138
+ cwd?: string;
139
+ }
140
+ /** Resolve one agent definition, by explicit file or by name search. Throws a clear error when
141
+ * neither locates a def — never returns a partial/empty result. */
142
+ declare function resolveAgentDef(input: ResolveAgentDefInput): AgentDef;
143
+ /** List every resolvable agent def under `.agents/agents/` (+ any extra search roots). Definitive
144
+ * empty array when the dir is absent or empty — never throws for "none found" here (unlike a
145
+ * named resolve, listing is inherently zero-or-more). */
146
+ declare function listAgentDefs(input?: {
147
+ cwd?: string;
148
+ searchRoots?: string[];
149
+ }): AgentDef[];
150
+ //#endregion
151
+ //#region src/agentdef/realize.d.ts
152
+ interface RealizeLaunchOptions {
153
+ /** Overrides def.model when present. */
154
+ model?: string;
155
+ /** Overrides def.harness when present; falls back to 'claude' when neither is set. */
156
+ harness?: Harness;
157
+ }
158
+ interface RealizedLaunch {
159
+ harness: Harness;
160
+ command: string;
161
+ }
162
+ /** Build the harness launch invocation for a def — explicit `model`/`harness` win over the def's
163
+ * own tags, which win over the harness default. Every harness gets the same shape (bin,
164
+ * `--model` when known, `--append-system-prompt` carrying the def's instructions body) since the
165
+ * exact per-harness flag surface is realized later by the launching caller, not by cyberlegion. */
166
+ declare function realizeLaunch(def: AgentDef, opts?: RealizeLaunchOptions): RealizedLaunch;
167
+ interface RealizeSubagentOptions {
168
+ /** File the spawned subagent reads its brief from. */
169
+ briefFile: string;
170
+ /** File the spawned subagent writes its result JSON to. */
171
+ resultFile: string;
172
+ }
173
+ /** Build the instruction string a parent model uses to spawn its OWN harness Task subagent for
174
+ * `def` — cyberlegion never invokes the Task tool itself. Names `subagent_type: <def.name>` for a
175
+ * harness that recognizes it, but always inlines the model + full instructions too so the same
176
+ * instruction string is correct even when the harness has no such named subagent type. */
177
+ declare function realizeSubagentInstruction(def: AgentDef, opts: RealizeSubagentOptions): string;
178
+ //#endregion
179
+ //#region src/console/session.d.ts
180
+ /** Generic multiplexer seam — no legion-specific concepts, so this composes with any host. */
181
+ /** Where a new pane/window/session should be placed relative to the caller's current one. */
182
+ type SessionPlacement = 'pane:right' | 'pane:down' | 'tab' | 'window';
183
+ interface SessionOpenOptions {
184
+ /** Working directory the new pane/window/session should start in. */
185
+ cwd: string;
186
+ /** Command line to launch inside the new pane once it is open. */
187
+ launch: string;
188
+ /** Placement relative to the caller; defaults to 'pane:right'. */
189
+ at?: SessionPlacement;
190
+ }
191
+ /** Opaque handle to an open pane/window/session; backend-specific id lives in `id`. */
192
+ interface SessionTarget {
193
+ id: string;
194
+ }
195
+ interface SessionReadOptions {
196
+ /** How many trailing lines of output to capture; omit for the backend's default. */
197
+ lines?: number;
198
+ }
199
+ interface SessionAdapter {
200
+ /** Backend name, e.g. "tmux" or "herdr". */
201
+ readonly name: string;
202
+ /** Create a new pane/window running `opts.launch` in `opts.cwd`; returns its target handle. */
203
+ open(exec: Exec, opts: SessionOpenOptions): SessionTarget;
204
+ /** Type text into the target session (submitted, not queued). */
205
+ send(exec: Exec, target: SessionTarget, text: string): void;
206
+ /** Capture the target session's current output. */
207
+ read(exec: Exec, target: SessionTarget, opts?: SessionReadOptions): string;
208
+ /** Move input focus to the target session; best-effort (may no-op if the backend can't). */
209
+ focus(exec: Exec, target: SessionTarget): void;
210
+ /** Close the target session. */
211
+ teardown(exec: Exec, target: SessionTarget): void;
212
+ }
213
+ //#endregion
214
+ //#region src/console/index.d.ts
215
+ /**
216
+ * Backend selection via the two-mode mux probe (`$CYBERLEGION_MUX` fast-path/override, else
217
+ * ancestry discovery from `$$` falling back to the `$TMUX`/`$HERDR_ENV` hint when the walk is
218
+ * inconclusive) — tmux/herdr map to their existing adapters; anything else is a clear error.
219
+ */
220
+ declare function selectSessionAdapter(env: NodeJS.ProcessEnv, exec?: Exec): SessionAdapter;
221
+ //#endregion
222
+ //#region src/console/mux-probe.d.ts
223
+ type Mux = 'tmux' | 'herdr' | 'screen' | 'none';
224
+ interface MuxProbe {
225
+ mux: Mux;
226
+ pane?: string;
227
+ /** 'env' — trusted the $CYBERLEGION_MUX fast-path/override. 'ancestry' — walked the process tree. */
228
+ via: 'env' | 'ancestry';
229
+ }
230
+ /**
231
+ * Two-mode multiplexer detection.
232
+ *
233
+ * Fast-path: `$CYBERLEGION_MUX` (tmux | herdr | screen | none) is trusted outright — this also
234
+ * serves as an OVERRIDE (`=none` forces no-mux even inside a real multiplexer). `$CYBERLEGION_MUX_PANE`
235
+ * carries the pane id alongside it.
236
+ *
237
+ * Discovery (else): walk the process ancestry from `$$` via `ps -o ppid=,comm= -p <pid>`, since the
238
+ * tool's own shell may not be the human's pane. `$TMUX`/`$HERDR_ENV` are NOT trusted alone — they
239
+ * are used only as a fast-positive hint the ancestry walk falls back to when the walk itself is
240
+ * inconclusive (e.g. `ps` unavailable), never as a substitute for it.
241
+ */
242
+ declare function probeMultiplexer(exec: Exec, env: NodeJS.ProcessEnv, opts?: {
243
+ discover?: boolean;
244
+ }): MuxProbe;
245
+ //#endregion
246
+ //#region src/console/worktree.d.ts
247
+ /** Generic multiplexer/worktree seam — no legion-specific concepts. */
248
+ interface WorktreeAddOptions {
249
+ /** The primary checkout's root — the repo `git worktree add` runs against. */
250
+ primaryRoot: string;
251
+ /** Where the new worktree should be checked out. */
252
+ path: string;
253
+ /** Branch to create the worktree on. */
254
+ branch: string;
255
+ }
256
+ interface Worktree {
257
+ root: string;
258
+ branch: string;
259
+ }
260
+ interface WorktreeRemoveOptions {
261
+ primaryRoot: string;
262
+ }
263
+ interface WorktreeAdapter {
264
+ add(exec: Exec, opts: WorktreeAddOptions): Worktree;
265
+ remove(exec: Exec, path: string, opts: WorktreeRemoveOptions): void;
266
+ }
267
+ /** The only worktree backend at MVP — plain `git worktree`. */
268
+ declare const gitWorktreeAdapter: WorktreeAdapter;
269
+ /**
270
+ * Resolve the primary checkout's root regardless of whether the caller's cwd is the primary
271
+ * checkout or a linked worktree — `--git-common-dir` always points at the main repo's `.git`.
272
+ */
273
+ declare function resolvePrimaryRoot(exec: Exec): string;
274
+ /**
275
+ * Refuse the primary checkout: a spawned unit's resolved worktree root must never be the primary
276
+ * checkout itself. A unit is never run in the primary checkout.
277
+ */
278
+ declare function assertDistinctFromPrimary(worktreeRoot: string, primaryRoot: string): void;
279
+ //#endregion
280
+ //#region src/decommission.d.ts
281
+ interface DecommissionInput {
282
+ id: string;
283
+ /** Discard uncommitted changes in the worktree. Never overrides refusing the primary checkout. */
284
+ force?: boolean;
285
+ }
286
+ interface DecommissionResult {
287
+ agent: AgentRecord;
288
+ worktreeRoot?: string;
289
+ pane?: string;
290
+ }
291
+ /**
292
+ * Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses
293
+ * the primary checkout (absolute — `--force` never overrides it) and a dirty worktree unless
294
+ * `--force`. Teardown always precedes reap: an already-gone worktree or pane is tolerated, but a
295
+ * genuine worktree-removal failure aborts and leaves the record intact so the operation is
296
+ * retryable.
297
+ */
298
+ declare function decommission(ctx: IdContext, input: DecommissionInput): DecommissionResult;
299
+ //#endregion
300
+ //#region src/dispatch/prep.d.ts
301
+ interface DispatchContext {
302
+ store: Store;
303
+ }
304
+ interface PrepInput {
305
+ /** Resolve an agent def (`.agents/agents/<name>.md`) to build the instruction from. */
306
+ agent?: string;
307
+ /** Read an exact agent def file instead of resolving by name (plugin-scoped escape hatch). */
308
+ agentFile?: string;
309
+ /** Role label folded into the generic instruction when no agent def is given. */
310
+ role?: string;
311
+ /** Brief body text. */
312
+ briefText?: string;
313
+ /** Read the brief from a file, or `-` for stdin. */
314
+ briefFile?: string;
315
+ /** JSON schema file the eventual result must satisfy — carried through by callers that validate. */
316
+ verdictSchema?: string;
317
+ /** Thread id correlating a channel-path reply; defaults to the minted dispatch id. */
318
+ thread?: string;
319
+ }
320
+ interface DispatchEnvelope {
321
+ id: string;
322
+ thread: string;
323
+ briefFile: string;
324
+ resultFile: string;
325
+ instruction: string;
326
+ }
327
+ /**
328
+ * Allocate a dispatch: mint an id (thread defaults to it), write the brief into the Store, compute
329
+ * the result-slot path, and build the instruction from a resolved agent def (or a generic one).
330
+ * Spawns nothing — the envelope is handed to whatever spawns next (a Task tool, `channel()`, a
331
+ * human).
332
+ */
333
+ declare function prep(ctx: DispatchContext, input: PrepInput, readStdin?: () => string): DispatchEnvelope;
334
+ //#endregion
335
+ //#region src/dispatch/channel.d.ts
336
+ interface ChannelInput extends PrepInput {
337
+ /** Block for the mail-thread reply and return a validated `DispatchResult`; without it, the peer
338
+ * is spawned and the envelope is returned so the caller can `mail await` later. */
339
+ wait?: boolean;
340
+ /** Placement relative to the caller; defaults to 'pane:right'. */
341
+ at?: SessionPlacement;
342
+ timeoutMs?: number;
343
+ maxWaitS?: number;
344
+ }
345
+ interface DispatchResult {
346
+ id: string;
347
+ verdict?: unknown;
348
+ body: string;
349
+ ts: number;
350
+ }
351
+ /** Thrown by a `--wait` channel call when its internal poll-cycle cap is hit with no reply yet — a
352
+ * clean, re-armable sentinel (mirrors `mail await`'s "waiting" outcome), never a failure. */
353
+ declare class DispatchWaitingError extends Error {
354
+ readonly thread: string;
355
+ constructor(thread: string);
356
+ }
357
+ /**
358
+ * `prep()` + spawn a peer session realized from the same agent def + (optionally) `awaitReply` on
359
+ * the thread. Requires `--agent`/`--agent-file` (a channel peer needs a real launch to realize).
360
+ */
361
+ declare function channel(ctx: IdContext, input: ChannelInput, readStdin?: () => string): Promise<DispatchResult | DispatchEnvelope>;
362
+ //#endregion
363
+ //#region src/dispatch/collect.d.ts
364
+ interface CollectContext {
365
+ store: Store;
366
+ now?: () => number;
367
+ }
368
+ /** Read + validate a dispatch's result file. Throws (fail-loud) when the file is not yet written,
369
+ * or when it fails the optional verdict schema. */
370
+ declare function collect(ctx: CollectContext, id: string, verdictSchema?: string): DispatchResult;
371
+ //#endregion
372
+ //#region src/runtime/inject-inbox.d.ts
373
+ type HookEvent = 'SessionStart' | 'PostToolUse';
374
+ interface InjectPayload {
375
+ hookSpecificOutput: {
376
+ hookEventName: HookEvent;
377
+ additionalContext: string;
378
+ };
379
+ }
380
+ /**
381
+ * Resolve the calling agent, gather its pending brief + unread mail, and return the
382
+ * SessionStart-style injection payload — or null when there is nothing to inject (an
383
+ * unregistered caller or an empty inbox never fails the harness hook).
384
+ */
385
+ declare function injectInbox(ctx: IdContext, event: string): InjectPayload | null;
386
+ //#endregion
387
+ //#region src/install.d.ts
388
+ type InstallStatus = 'registered' | 'already present';
389
+ interface InstallResult {
390
+ harness: Harness;
391
+ event: HookEvent;
392
+ vendorEvent: string;
393
+ file: string;
394
+ status: InstallStatus;
395
+ }
396
+ /** Register the surfacing hook into one harness's config, idempotently. */
397
+ declare function install(harness: Harness, projectDir?: string): InstallResult[];
398
+ //#endregion
399
+ //#region src/message.d.ts
400
+ interface MsgContext {
401
+ store: Store;
402
+ now?: () => number;
403
+ }
404
+ interface SendInput {
405
+ fromId: string;
406
+ to: string;
407
+ subject?: string;
408
+ body: string;
409
+ thread?: string;
410
+ replyTo?: string;
411
+ }
412
+ /** Write one message into the recipient's inbox. Collision-free by <epochMs>-<hex>. */
413
+ declare function send(ctx: MsgContext, input: SendInput): Message;
414
+ /** Resolve a message body from the --body flag, a --body-file path, or stdin (--body-file -). */
415
+ declare function resolveBody(body: string | undefined, bodyFile: string | undefined, readStdin?: () => string): string;
416
+ interface InboxItem extends Message {
417
+ read: boolean;
418
+ }
419
+ interface InboxQuery {
420
+ meId: string;
421
+ unread?: boolean;
422
+ from?: string;
423
+ /** Filter to messages carrying this thread id; a message with no thread is excluded. */
424
+ thread?: string;
425
+ }
426
+ /** List the caller's mail, chronological (lexical id sort == time order). */
427
+ declare function inbox(ctx: MsgContext, q: InboxQuery): InboxItem[];
428
+ /** Peek at a message (unread or already-acked) without changing its state. */
429
+ declare function peek(ctx: MsgContext, meId: string, msgId: string): Message | undefined;
430
+ /** Acknowledge a message by moving it out of the unread set. Errors if not currently unread. */
431
+ declare function ack(ctx: MsgContext, meId: string, msgId: string): Message;
432
+ /** Permanently remove a message (unread or already-acked) from the caller's inbox. */
433
+ declare function deleteMessage(ctx: MsgContext, meId: string, msgId: string): void;
434
+ //#endregion
435
+ //#region src/output.d.ts
436
+ type Format = 'toon' | 'json';
437
+ /** One TOON-encoded object: `key: value` lines, blank/undefined fields dropped. */
438
+ declare function toonObject(fields: Record<string, unknown>): string;
439
+ /** One TOON-encoded list: a `name[N]{field,...}:` header plus one row per item, then an aggregate
440
+ * summary line. Definitive on empty — still emits `name[0]{...}:` plus the summary line. */
441
+ declare function toonList<T>(name: string, items: T[], fields: {
442
+ key: string;
443
+ get: (item: T) => unknown;
444
+ }[], summary: string): string;
445
+ interface EmitPayload<T = unknown> {
446
+ /** The TOON-rendered string (already includes any aggregate/summary line). */
447
+ toon: string;
448
+ /** The structured value emitted verbatim as JSON under `--format json`. */
449
+ json: T;
450
+ }
451
+ /** Print a command's result to stdout in the requested format. */
452
+ declare function emit(format: Format, payload: EmitPayload): void;
453
+ /** A stderr-only next-step suggestion — never part of the machine-readable stdout result. */
454
+ declare function nextStep(msg: string): void;
455
+ /** A structured, fail-loud error: stderr + exit 1. Never prompts, never partially writes. */
456
+ declare function fail(msg: string): never;
457
+ //#endregion
458
+ //#region src/paths.d.ts
459
+ /** Walk up from `cwd` to the nearest git repo root; fall back to `cwd`. */
460
+ declare function projectRoot(cwd?: string): string;
461
+ /** Runs a command synchronously in `cwd`; returns trimmed stdout, or null on any failure. */
462
+ type Exec$1 = (cmd: string, args: string[]) => string | null;
463
+ interface RootOptions {
464
+ root?: string;
465
+ space?: string;
466
+ cwd?: string;
467
+ env?: NodeJS.ProcessEnv;
468
+ exec?: Exec$1;
469
+ }
470
+ /**
471
+ * Resolve the cyberlegion hub root. Precedence: explicit --root/--space, then `$CYBERLEGION_ROOT`,
472
+ * then the GLOBAL hub `~/.agents/cyberlegion` (addressable across every project and worktree
473
+ * boundary — identity/mail/registry state lives here by default; `--space` isolates it), falling
474
+ * back to a project-local `.agents/cyberlegion` only when no home directory is resolvable.
475
+ */
476
+ declare function resolveRoot(opts?: RootOptions): string;
477
+ /**
478
+ * Resolve the primary checkout's project-local cyberlegion dir — where spawned unit worktrees
479
+ * live (never the global hub), keyed off `git worktree`'s common-dir so it resolves the same from
480
+ * the primary checkout or from any of its linked worktrees.
481
+ */
482
+ declare function resolveProjectLocalRoot(opts?: RootOptions): string;
483
+ /**
484
+ * Create the tracked `config.json` marker at `root` (a hub root) if it does not already exist.
485
+ * Callers pass the root under which the marker should be created/ensured; this mkdir's it as
486
+ * needed. Idempotent — never overwrites an existing marker.
487
+ */
488
+ declare function ensureMarker(root: string): void;
489
+ declare const paths: {
490
+ agentsDir: (root: string) => string;
491
+ agentFile: (root: string, id: string) => string;
492
+ panesDir: (root: string) => string;
493
+ paneFile: (root: string, pane: string) => string;
494
+ inboxDir: (root: string, id: string) => string;
495
+ inboxReadDir: (root: string, id: string) => string;
496
+ dataDir: (root: string, id: string) => string;
497
+ briefFile: (root: string, id: string) => string;
498
+ resultFile: (root: string, id: string) => string;
499
+ worktreeDir: (root: string, id: string) => string;
500
+ };
501
+ //#endregion
502
+ //#region src/session.d.ts
503
+ interface SpawnInput {
504
+ harness?: string;
505
+ task?: string;
506
+ briefFile?: string;
507
+ handle?: string;
508
+ /** Override the default per-harness launch command (e.g. an agent-def's `realizeLaunch` output,
509
+ * carrying `--model`/`--append-system-prompt`) — composed in by callers, never by `spawn` itself
510
+ * (avoids a session.ts ↔ agentdef/realize.ts import cycle, since `realizeLaunch` already reads
511
+ * `LAUNCH_MAP` from here). */
512
+ command?: string;
513
+ /** Branch to create the unit's worktree on; defaults to `cyberlegion/unit-<id>`. */
514
+ branch?: string;
515
+ /** Where to check out the unit's worktree; defaults under `<primary>/.agents/cyberlegion/worktrees/<id>`. */
516
+ worktreePath?: string;
517
+ /** Placement relative to the caller; defaults to 'pane:right'. */
518
+ at?: SessionPlacement;
519
+ }
520
+ interface SpawnResult {
521
+ agent: AgentRecord;
522
+ pane: string;
523
+ launch: string;
524
+ }
525
+ /**
526
+ * Launch a new peer session as a genuine sibling unit: create a real git worktree distinct from
527
+ * the primary checkout (refuse the primary checkout), open a session backend (tmux or herdr) with
528
+ * its cwd set to that worktree, pre-register the peer, and drop its brief as a file the peer's own
529
+ * SessionStart hook reads — never typed into its prompt.
530
+ *
531
+ * Spawned unit worktrees live project-local (`<primary>/.agents/cyberlegion/worktrees/<id>`), even
532
+ * though the registry/mailbox itself lives in the global hub — the hub addresses units across
533
+ * project and worktree boundaries, but a unit's checkout is necessarily project-local.
534
+ */
535
+ declare function spawn(ctx: IdContext, input: SpawnInput): SpawnResult;
536
+ //#endregion
537
+ //#region src/store/file-store.d.ts
538
+ /** The on-disk `Store` implementation — current per-writer sharded `.json` layout (ADR-0020):
539
+ * one file per message/agent, collision-free filenames, ack = atomic rename into `read/`. */
540
+ declare class FileStore implements Store {
541
+ readonly root: string;
542
+ constructor(root: string);
543
+ ensureMarker(): void;
544
+ putMessage(toId: string, msg: Message): void;
545
+ listInbox(id: string): InboxSnapshot;
546
+ ackMessage(id: string, msgId: string): Message;
547
+ removeMessage(id: string, msgId: string): void;
548
+ putAgent(rec: AgentRecord): void;
549
+ getAgent(id: string): AgentRecord | undefined;
550
+ listAgents(): AgentRecord[];
551
+ removeAgent(id: string): void;
552
+ removeAgentData(id: string): void;
553
+ putPaneIndex(pane: string, agentId: string): void;
554
+ resolvePaneId(pane: string): string | undefined;
555
+ findPaneByAgentId(agentId: string): string | undefined;
556
+ removePaneIndex(pane: string): void;
557
+ writeBrief(agentId: string, text: string): void;
558
+ readBrief(agentId: string): string | undefined;
559
+ resultPath(id: string): string;
560
+ writeResult(id: string, value: unknown): void;
561
+ readResult(id: string): string | undefined;
562
+ }
563
+ //#endregion
564
+ //#region src/wake/await.d.ts
565
+ interface AwaitInput {
566
+ meId: string;
567
+ thread: string;
568
+ from?: string;
569
+ /** Give up after this many ms with no match; 0 = wait forever. Default 600_000 (600s). */
570
+ timeoutMs?: number;
571
+ /** Self-cap for one internal poll cycle, in seconds. Default 240 (well under a harness tool-timeout). */
572
+ maxWaitS?: number;
573
+ now?: () => number;
574
+ sleep?: (ms: number) => Promise<void>;
575
+ }
576
+ type AwaitOutcome = {
577
+ kind: 'matched';
578
+ message: InboxItem;
579
+ } | {
580
+ kind: 'waiting';
581
+ } | {
582
+ kind: 'timed-out';
583
+ };
584
+ /**
585
+ * Block on the caller's UNREAD inbox until a message matching `thread` (and optional `from`)
586
+ * arrives, polling on a fixed ~1s internal interval. Three unambiguous outcomes:
587
+ *
588
+ * - `matched` — a message arrived; it is printed by the caller AND acked here (moved to read/), so
589
+ * it leaves the unread set. Block-then-read: each await consumes exactly its round.
590
+ * - `waiting` — this call's internal poll cycle hit its `maxWaitS` cap (default 240s, always well
591
+ * under a harness tool-timeout SIGKILL) with no match yet. A clean, non-error return the caller
592
+ * re-arms on (calls `awaitReply` again) — NOT the same as giving up.
593
+ * - `timed-out` — the caller's overall `timeoutMs` (default 600_000ms / 600s; `0` = wait forever)
594
+ * elapsed across re-arms with no match. The caller should exit non-zero.
595
+ */
596
+ declare function awaitReply(ctx: MsgContext, input: AwaitInput): Promise<AwaitOutcome>;
597
+ //#endregion
598
+ //#region src/wake/wake-path.d.ts
599
+ /**
600
+ * The wake-matrix path a gateway would drive a turn through. Pure decision helper — no I/O, no
601
+ * polling, no session backend calls; the gateway (`legion-gateway-legate`, CR-5) composes this with
602
+ * the actual `mail await` / `session nudge` primitives.
603
+ *
604
+ * - **A** — a single unbounded `mail await` (not returned here; superseded by A-loop everywhere the
605
+ * gateway drives this decision, since a raw A risks a harness tool-timeout SIGKILL).
606
+ * - **A-loop** — the portable default: a bounded `mail await` cycle, re-armed by the caller.
607
+ * - **A-prime** — Claude Code with an observable background task: run `await` via
608
+ * `run_in_background` and poll its result instead of blocking the turn.
609
+ * - **B** — a live foreign session behind a verified multiplexer: ring the doorbell
610
+ * (`session nudge`) instead of polling. Never selected without a mux.
611
+ * - **C** — own-the-PTY (dropped from the shipped matrix; kept as a type for completeness).
612
+ * - **D** — `/loop`-style external re-invocation (dropped from the shipped matrix).
613
+ * - **E** — hook surfacing at a turn boundary (selected by the caller directly, not by this
614
+ * function — it has no wait/poll shape to decide between).
615
+ */
616
+ type WakePath = 'A' | 'A-loop' | 'A-prime' | 'B' | 'C' | 'D' | 'E';
617
+ interface WakePathInput {
618
+ /** The calling harness, e.g. 'claude' | 'cursor' | 'codex'. */
619
+ harness: string;
620
+ mux: MuxProbe;
621
+ /** The harness can observe a backgrounded task's result without blocking the turn. */
622
+ observable?: boolean;
623
+ /** The target is a live, foreign interactive session that can be rung rather than polled. */
624
+ dedicatedListener?: boolean;
625
+ }
626
+ interface WakePathResult {
627
+ path: WakePath;
628
+ why: string;
629
+ }
630
+ /**
631
+ * Select a wake path from the shipped matrix. Rule order: a `mux.mux === 'none'` caller can never
632
+ * get **B** — it always falls through to the bounded poll. Claude Code + an observable background
633
+ * task prefers **A-prime**. A live foreign session behind a verified mux prefers **B**. Otherwise
634
+ * the portable default is **A-loop**.
635
+ */
636
+ declare function selectWakePath(input: WakePathInput): WakePathResult;
637
+ //#endregion
638
+ //#region src/wake/watch.d.ts
639
+ interface WatchInput {
640
+ meId: string;
641
+ thread?: string;
642
+ from?: string;
643
+ }
644
+ interface WatchOptions {
645
+ sleep?: (ms: number) => Promise<void>;
646
+ /** Bounds the loop for tests; omitted (default) runs until the process is interrupted. */
647
+ maxIterations?: number;
648
+ }
649
+ /**
650
+ * A continuous foreground observer: prints each NEW matching message as it arrives, polling on a
651
+ * fixed ~1s interval, until interrupted (or `maxIterations` in tests). Never acks — a message
652
+ * `watch` prints stays unread and still surfaces later in `mail inbox`/`mail await`.
653
+ */
654
+ declare function watchMail(ctx: MsgContext, input: WatchInput, onMessage: (msg: InboxItem) => void, opts?: WatchOptions): Promise<void>;
655
+ //#endregion
656
+ 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 };
package/dist/index.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import { A as resolveProjectLocalRoot, B as loadAgent, C as resolveAgentDef, D as ensureMarker, E as spawn, F as selectSessionAdapter, G as resolveRecipient, H as realExec, I as probeMultiplexer, K as resolveSelfId, L as bumpLastSeen, M as assertDistinctFromPrimary, N as gitWorktreeAdapter, O as paths, P as resolvePrimaryRoot, R as detectHarness, S as listAgentDefs, T as realizeSubagentInstruction, U as register, V as prune, W as resolveAgent, _ 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 resolveRoot, k as projectRoot, l as install, m as awaitReply, n as FileStore, o as nextStep, p as prep, q as touch, 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 listAgents } from "./watch-DA8I44dv.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 };