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.
- package/bin/cyberlegion.mjs +6 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +720 -0
- package/dist/index.d.mts +656 -0
- package/dist/index.mjs +28 -0
- package/dist/watch-DA8I44dv.mjs +1403 -0
- package/package.json +61 -0
package/dist/index.d.mts
ADDED
|
@@ -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 };
|