cyberlegion 0.2.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/bin/cyberlegion.mjs +2 -1
- package/dist/cli.d.mts +12 -1
- package/dist/cli.mjs +72 -172
- package/dist/index.d.mts +50 -9
- package/dist/index.mjs +2 -2
- package/dist/{watch-nkm6F1_f.mjs → watch-BZmEAqUo.mjs} +390 -159
- package/package.json +1 -1
package/bin/cyberlegion.mjs
CHANGED
|
@@ -3,4 +3,5 @@ import { dirname, join } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
|
|
5
5
|
const dir = dirname(fileURLToPath(import.meta.url))
|
|
6
|
-
await import(join(dir, '..', 'dist', 'cli.mjs'))
|
|
6
|
+
const { runCli } = await import(join(dir, '..', 'dist', 'cli.mjs'))
|
|
7
|
+
await runCli()
|
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Run the CLI. Exported rather than executed at module scope so the command bodies are reachable
|
|
4
|
+
* from a test: with a bare `program.parseAsync(process.argv)` here, importing this module ran the
|
|
5
|
+
* whole CLI, so every `.action()` body — and every option wire inside it — was unreachable, and a
|
|
6
|
+
* flag that stopped being forwarded changed real behavior with the suite green.
|
|
7
|
+
*
|
|
8
|
+
* `bin/cyberlegion.mjs` calls this; nothing else should.
|
|
9
|
+
*/
|
|
10
|
+
declare function runCli(argv?: string[]): Promise<void>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { runCli };
|
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as bumpLastSeen, B as registerStanding, C as nudgeUnit, D as selectSessionAdapter, E as spawnAndWake, F as loadAgent, G as resolveStandingOwner, H as resolvePresence, I as prune, J as listAgentDefs, K as touch, L as realExec, M as clearPresence, N as detectHarness, O as DELIVERY_DOORBELL, P as listAgents, R as reconcile, S as focusUnit, V as resolveAgent, W as resolveSelfId, Y as resolveAgentDef, _ as install, a as emit, b as resolveSpawnLaunch, c as toonList, d as deleteMessage, et as resolveRoot, f as inbox, g as send, h as resolveBody, i as injectInbox, j as claimPresence, k as wakeRecipient, l as toonObject, m as readAck, n as awaitReply, o as fail, p as peek, q as normalizeMuxEnv, r as FileStore, s as nextStep, t as watchMail, u as ack, v as decommission, w as readUnit, x as clearUnit, z as register } from "./watch-BZmEAqUo.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
|
-
import { currentPane,
|
|
5
|
+
import { currentPane, probeMultiplexer } from "cyber-mux";
|
|
6
6
|
//#region src/admin.ts
|
|
7
7
|
/**
|
|
8
8
|
* Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
|
|
@@ -38,117 +38,39 @@ function migrateStore(from, to) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
//#endregion
|
|
41
|
-
//#region src/
|
|
42
|
-
/** The doorbell text delivered to a woken recipient; also the standalone `unit nudge` default. */
|
|
43
|
-
const DELIVERY_DOORBELL = "You have unread mail — check your inbox.";
|
|
41
|
+
//#region src/cli-input.ts
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* without a human nudge.
|
|
49
|
-
*/
|
|
50
|
-
const SPAWN_DOORBELL = "Your brief is loaded in context — read it and begin work.";
|
|
51
|
-
/**
|
|
52
|
-
* A freshly-launched harness cold-boots slower than an already-running peer, so the spawn first-turn
|
|
53
|
-
* ring gets a wider retry budget than a plain `mail send` doorbell (nudge's own 10 × 400ms): flush the
|
|
54
|
-
* staged buffer for up to ~8s (20 × 400ms) before giving up. The common case still returns after one
|
|
55
|
-
* settle cycle once the peer takes the turn — the budget only bounds a slow or stuck boot. Still
|
|
56
|
-
* bounded — a harness that never reaches its prompt resolves to a best-effort warning, never a hang.
|
|
57
|
-
*/
|
|
58
|
-
const SPAWN_NUDGE_OPTS = {
|
|
59
|
-
attempts: 20,
|
|
60
|
-
settleMs: 400
|
|
61
|
-
};
|
|
62
|
-
/** Resolve an agent's live session pane: its recorded pane, else a pane pointer keyed by its id. */
|
|
63
|
-
function paneOf(store, id) {
|
|
64
|
-
return loadAgent(store, id)?.pane?.id ?? store.findPaneByAgentId(id);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Best-effort wake the recipient of a just-delivered message so it checks its inbox. A peer's live
|
|
68
|
-
* session pane, a standing owner's bound presence (`unit claim` — the live unit standing in for it,
|
|
69
|
-
* an exited one falling back below), or — with neither — the hub's bound main pane, is rung via the
|
|
70
|
-
* nudge submit-verify path (a taken turn, not fire-and-forget). Durable delivery already happened;
|
|
71
|
-
* the ring is opportunistic on top, so a legitimate no-op (`--no-nudge`, a headless/absent recipient
|
|
72
|
-
* with no live pane, a standing-owner send with no presence and no main pane bound, or a
|
|
73
|
-
* self-addressed send) rings nothing, and a ring that never completes within nudge's retry cap is
|
|
74
|
-
* swallowed into a warning. This never throws — it can never fail the send.
|
|
43
|
+
* Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
|
|
44
|
+
* `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
|
|
45
|
+
* `--harness` overriding the def's own.
|
|
75
46
|
*
|
|
76
|
-
*
|
|
77
|
-
* inside the same swallowing try — so a session with no mux backend (where `selectSessionAdapter`
|
|
78
|
-
* throws) is just a no-op wake, never a failed send.
|
|
47
|
+
* Throws when no harness can be resolved, so the CLI's own `fail()` still renders it.
|
|
79
48
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
focused = void 0;
|
|
102
|
-
}
|
|
103
|
-
if (focused === false) return {
|
|
104
|
-
rung: false,
|
|
105
|
-
pane
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
await nudge(getAdapter(), exec, { id: pane }, DELIVERY_DOORBELL, nudgeOpts);
|
|
110
|
-
return {
|
|
111
|
-
rung: true,
|
|
112
|
-
pane
|
|
113
|
-
};
|
|
114
|
-
} catch (err) {
|
|
115
|
-
return {
|
|
116
|
-
rung: false,
|
|
117
|
-
pane,
|
|
118
|
-
warning: err instanceof Error ? err.message : String(err)
|
|
119
|
-
};
|
|
120
|
-
}
|
|
49
|
+
function spawnCommandInput(opts) {
|
|
50
|
+
const { harness, command } = resolveSpawnLaunch({
|
|
51
|
+
agent: opts.agent,
|
|
52
|
+
agentFile: opts.agentFile,
|
|
53
|
+
harness: opts.harness
|
|
54
|
+
});
|
|
55
|
+
if (!harness) throw new Error("unit spawn needs --harness, or --agent/--agent-file resolving one");
|
|
56
|
+
return {
|
|
57
|
+
input: {
|
|
58
|
+
harness,
|
|
59
|
+
command,
|
|
60
|
+
task: opts.task,
|
|
61
|
+
briefFile: opts.briefFile,
|
|
62
|
+
handle: opts.handle,
|
|
63
|
+
branch: opts.branch,
|
|
64
|
+
worktreePath: opts.worktreePath,
|
|
65
|
+
cwd: opts.cwd,
|
|
66
|
+
at: opts.at
|
|
67
|
+
},
|
|
68
|
+
noWake: opts.wake === false
|
|
69
|
+
};
|
|
121
70
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* context by its own SessionStart hook, but the model takes no turn on its own, unlike a subagent
|
|
126
|
-
* (where the caller's Task call IS the turn). So `unit spawn` rings the `SPAWN_DOORBELL` over the
|
|
127
|
-
* boot-race-aware `nudge` submit-verify path (a taken turn, never re-typing the brief), the same
|
|
128
|
-
* best-effort ring `wakeRecipient` gives `mail send`: the spawn (worktree, session, registry record)
|
|
129
|
-
* is the guaranteed effect and the ring is opportunistic on top, so `--no-wake` rings nothing and a
|
|
130
|
-
* ring that never completes within the retry budget is swallowed into a warning. This never throws —
|
|
131
|
-
* it can never fail the spawn.
|
|
132
|
-
*
|
|
133
|
-
* The adapter is resolved lazily via `getAdapter` inside the same swallowing try, so even a session
|
|
134
|
-
* whose backend has since gone away (where `selectSessionAdapter` would throw) degrades to a warned
|
|
135
|
-
* no-op rather than a failed spawn.
|
|
136
|
-
*/
|
|
137
|
-
async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS) {
|
|
138
|
-
if (input.noWake) return { rung: false };
|
|
139
|
-
try {
|
|
140
|
-
await nudge(getAdapter(), exec, input.target, SPAWN_DOORBELL, nudgeOpts);
|
|
141
|
-
return {
|
|
142
|
-
rung: true,
|
|
143
|
-
pane: input.target.id
|
|
144
|
-
};
|
|
145
|
-
} catch (err) {
|
|
146
|
-
return {
|
|
147
|
-
rung: false,
|
|
148
|
-
pane: input.target.id,
|
|
149
|
-
warning: err instanceof Error ? err.message : String(err)
|
|
150
|
-
};
|
|
151
|
-
}
|
|
71
|
+
/** What `unit read` prints: the raw scrape, or the JSON envelope under `--format json`. */
|
|
72
|
+
function readCommandOutput(format, result) {
|
|
73
|
+
return format === "json" ? JSON.stringify(result, null, 2) : result.output;
|
|
152
74
|
}
|
|
153
75
|
//#endregion
|
|
154
76
|
//#region src/cli.ts
|
|
@@ -168,12 +90,6 @@ function requireSelf(ctx) {
|
|
|
168
90
|
bumpLastSeen(ctx, id);
|
|
169
91
|
return id;
|
|
170
92
|
}
|
|
171
|
-
function resolveTarget(ctx, ref) {
|
|
172
|
-
const agent = resolveAgent(ctx.store, ref);
|
|
173
|
-
const pane = agent.pane?.id ?? ctx.store.findPaneByAgentId(agent.id);
|
|
174
|
-
if (!pane) fail(`unit "${ref}" has no known session pane`);
|
|
175
|
-
return { id: pane };
|
|
176
|
-
}
|
|
177
93
|
function withGlobals(cmd) {
|
|
178
94
|
return cmd.option("--space <path>", "isolate the hub root (overrides the global hub / $CYBERLEGION_ROOT)").addOption(new Option("--format <format>", "output format").choices(["toon", "json"]).default("toon"));
|
|
179
95
|
}
|
|
@@ -357,39 +273,14 @@ function defineSpawn(cmd) {
|
|
|
357
273
|
])).option("--no-wake", "suppress the first-turn doorbell (spawn idle; the caller drives the first turn itself)").action(async (opts) => {
|
|
358
274
|
const ctx = ctxOf(opts);
|
|
359
275
|
touch(ctx);
|
|
360
|
-
let
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def = resolveAgentDef({
|
|
366
|
-
name: opts.agent,
|
|
367
|
-
file: opts.agentFile
|
|
368
|
-
});
|
|
369
|
-
} catch (err) {
|
|
370
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
371
|
-
}
|
|
372
|
-
const realized = realizeLaunch(def, { harness: opts.harness });
|
|
373
|
-
harness = realized.harness;
|
|
374
|
-
command = realized.command;
|
|
276
|
+
let spawnInput;
|
|
277
|
+
try {
|
|
278
|
+
spawnInput = spawnCommandInput(opts);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
375
281
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
harness,
|
|
379
|
-
command,
|
|
380
|
-
task: opts.task,
|
|
381
|
-
briefFile: opts.briefFile,
|
|
382
|
-
handle: opts.handle,
|
|
383
|
-
branch: opts.branch,
|
|
384
|
-
worktreePath: opts.worktreePath,
|
|
385
|
-
cwd: opts.cwd,
|
|
386
|
-
at: opts.at
|
|
387
|
-
});
|
|
388
|
-
const wake = await wakeSpawn(() => selectSessionAdapter(ctx.env ?? process.env), realExec, {
|
|
389
|
-
target: { id: res.pane },
|
|
390
|
-
noWake: opts.wake === false
|
|
391
|
-
});
|
|
392
|
-
if (wake.warning) console.error(`first-turn doorbell not confirmed (peer still spawned; nudge it manually): ${wake.warning}`);
|
|
282
|
+
const res = await spawnAndWake(ctx, spawnInput.input, { noWake: spawnInput.noWake });
|
|
283
|
+
if (res.warning) console.error(`first-turn doorbell not confirmed (peer still spawned; nudge it manually): ${res.warning}`);
|
|
393
284
|
emit(formatOf(opts), {
|
|
394
285
|
toon: toonObject({
|
|
395
286
|
spawned: res.agent.id,
|
|
@@ -397,11 +288,13 @@ function defineSpawn(cmd) {
|
|
|
397
288
|
harness: res.agent.harness,
|
|
398
289
|
worktree: res.agent.worktree?.root,
|
|
399
290
|
pane: res.pane,
|
|
400
|
-
rung:
|
|
291
|
+
rung: res.rung
|
|
401
292
|
}),
|
|
402
293
|
json: {
|
|
403
|
-
|
|
404
|
-
|
|
294
|
+
agent: res.agent,
|
|
295
|
+
pane: res.pane,
|
|
296
|
+
launch: res.launch,
|
|
297
|
+
rung: res.rung
|
|
405
298
|
}
|
|
406
299
|
});
|
|
407
300
|
nextStep(`cyberlegion unit read ${res.agent.id}`);
|
|
@@ -428,49 +321,44 @@ withGlobals(unit.command("close")).description("tear down a unit's worktree + se
|
|
|
428
321
|
withGlobals(unit.command("focus")).description("move input focus to a peer's session").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
429
322
|
const ctx = ctxOf(opts);
|
|
430
323
|
touch(ctx);
|
|
431
|
-
const
|
|
432
|
-
selectSessionAdapter(ctx.env ?? process.env).focus(realExec, target);
|
|
324
|
+
const { pane } = focusUnit(ctx, ref);
|
|
433
325
|
emit(formatOf(opts), {
|
|
434
326
|
toon: toonObject({
|
|
435
327
|
focused: ref,
|
|
436
|
-
pane
|
|
328
|
+
pane
|
|
437
329
|
}),
|
|
438
330
|
json: {
|
|
439
331
|
ref,
|
|
440
|
-
pane
|
|
332
|
+
pane
|
|
441
333
|
}
|
|
442
334
|
});
|
|
443
335
|
});
|
|
444
336
|
withGlobals(unit.command("nudge")).description("ring a peer's session (a doorbell that tells them to check their mail)").argument("<ref>", "unit id, handle, or worktree branch/CR ref").option("--message <text>", "the doorbell text delivered to the peer session", DELIVERY_DOORBELL).action(async (ref, opts) => {
|
|
445
337
|
const ctx = ctxOf(opts);
|
|
446
338
|
touch(ctx);
|
|
447
|
-
const
|
|
448
|
-
const message = opts.message || "You have unread mail — check your inbox.";
|
|
449
|
-
const result = await nudge(selectSessionAdapter(ctx.env ?? process.env), realExec, target, message);
|
|
339
|
+
const { pane, message, resubmits } = await nudgeUnit(ctx, ref, { message: opts.message });
|
|
450
340
|
emit(formatOf(opts), {
|
|
451
341
|
toon: toonObject({
|
|
452
342
|
nudged: ref,
|
|
453
|
-
pane
|
|
343
|
+
pane
|
|
454
344
|
}),
|
|
455
345
|
json: {
|
|
456
346
|
ref,
|
|
457
|
-
pane
|
|
347
|
+
pane,
|
|
458
348
|
message,
|
|
459
|
-
resubmits
|
|
349
|
+
resubmits
|
|
460
350
|
}
|
|
461
351
|
});
|
|
462
352
|
});
|
|
463
353
|
withGlobals(unit.command("read")).description("scrape a peer's session screen").argument("<ref>", "unit id, handle, or worktree branch/CR ref").option("--lines <n>", "trailing lines to capture", (v) => Number.parseInt(v, 10)).action((ref, opts) => {
|
|
464
354
|
const ctx = ctxOf(opts);
|
|
465
355
|
touch(ctx);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
if (formatOf(opts) === "json") console.log(JSON.stringify({
|
|
356
|
+
const { pane, output } = readUnit(ctx, ref, { lines: opts.lines });
|
|
357
|
+
console.log(readCommandOutput(formatOf(opts), {
|
|
469
358
|
ref,
|
|
470
|
-
pane
|
|
471
|
-
output
|
|
472
|
-
}
|
|
473
|
-
else console.log(text);
|
|
359
|
+
pane,
|
|
360
|
+
output
|
|
361
|
+
}));
|
|
474
362
|
});
|
|
475
363
|
withGlobals(unit.command("clear")).description("reset a warm peer's context to cold by injecting its own harness fresh-context command — keeps the pane/session warm; tears down nothing").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
476
364
|
const ctx = ctxOf(opts);
|
|
@@ -918,9 +806,21 @@ withGlobals(program).action((opts) => {
|
|
|
918
806
|
if (!id) nextStep("cyberlegion unit register to join");
|
|
919
807
|
else if (unread > 0) nextStep("cyberlegion mail inbox --unread");
|
|
920
808
|
});
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Run the CLI. Exported rather than executed at module scope so the command bodies are reachable
|
|
811
|
+
* from a test: with a bare `program.parseAsync(process.argv)` here, importing this module ran the
|
|
812
|
+
* whole CLI, so every `.action()` body — and every option wire inside it — was unreachable, and a
|
|
813
|
+
* flag that stopped being forwarded changed real behavior with the suite green.
|
|
814
|
+
*
|
|
815
|
+
* `bin/cyberlegion.mjs` calls this; nothing else should.
|
|
816
|
+
*/
|
|
817
|
+
async function runCli(argv = process.argv) {
|
|
818
|
+
try {
|
|
819
|
+
await program.parseAsync(argv);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
925
825
|
//#endregion
|
|
926
|
-
export {};
|
|
826
|
+
export { runCli };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Exec, Exec as Exec$1, MuxAdapter, MuxPlacement, MuxProbe, probeMultiplexer } from "cyber-mux";
|
|
1
|
+
import { Exec, Exec as Exec$1, MuxAdapter, MuxPlacement, MuxProbe, NudgeOptions, probeMultiplexer } from "cyber-mux";
|
|
2
2
|
import { assertDistinctFromPrimary, gitWorktreeAdapter, resolvePrimaryRoot } from "cyber-mux/worktree";
|
|
3
3
|
|
|
4
4
|
//#region src/store/store.d.ts
|
|
@@ -15,7 +15,10 @@ interface Message {
|
|
|
15
15
|
sentAt: string;
|
|
16
16
|
}
|
|
17
17
|
type Harness = 'claude' | 'cursor' | 'codex';
|
|
18
|
-
|
|
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';
|
|
19
22
|
interface AgentRecord {
|
|
20
23
|
id: string;
|
|
21
24
|
handle: string;
|
|
@@ -35,7 +38,19 @@ interface AgentRecord {
|
|
|
35
38
|
session?: string;
|
|
36
39
|
} | null;
|
|
37
40
|
pid?: number;
|
|
38
|
-
status
|
|
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 & {});
|
|
39
54
|
createdAt: string;
|
|
40
55
|
lastSeen: string;
|
|
41
56
|
brief?: string;
|
|
@@ -211,9 +226,13 @@ interface InjectPayload {
|
|
|
211
226
|
};
|
|
212
227
|
}
|
|
213
228
|
/**
|
|
214
|
-
* Resolve the calling agent, gather its
|
|
215
|
-
* SessionStart-style injection payload — or null when there is
|
|
216
|
-
* 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).
|
|
217
236
|
*/
|
|
218
237
|
declare function injectInbox(ctx: IdContext, event: string): InjectPayload | null;
|
|
219
238
|
//#endregion
|
|
@@ -378,8 +397,10 @@ interface SpawnResult {
|
|
|
378
397
|
/**
|
|
379
398
|
* Launch a new peer session as a genuine sibling unit: create a real git worktree distinct from
|
|
380
399
|
* the primary checkout (refuse the primary checkout), open a session backend (tmux or herdr) with
|
|
381
|
-
* its cwd set to that worktree, pre-register the peer, and drop its brief as a file
|
|
382
|
-
*
|
|
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`.
|
|
383
404
|
*
|
|
384
405
|
* Spawned unit worktrees live sibling to the primary checkout (`<parent>/<repo>.worktrees/legion-<id6>`),
|
|
385
406
|
* never nested inside it, even though the registry/mailbox itself lives in the global hub — the hub
|
|
@@ -387,6 +408,26 @@ interface SpawnResult {
|
|
|
387
408
|
* scoped to this one project's git remote.
|
|
388
409
|
*/
|
|
389
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
|
+
}>;
|
|
390
431
|
//#endregion
|
|
391
432
|
//#region src/store/file-store.d.ts
|
|
392
433
|
/** The on-disk `Store` implementation — current per-writer sharded `.json` layout (ADR-0020):
|
|
@@ -466,4 +507,4 @@ interface WatchOptions {
|
|
|
466
507
|
*/
|
|
467
508
|
declare function watchMail(ctx: MsgContext, input: WatchInput, onMessage: (msg: InboxItem) => void, opts?: WatchOptions): Promise<void>;
|
|
468
509
|
//#endregion
|
|
469
|
-
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, 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,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
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
2
|
import { probeMultiplexer } from "cyber-mux";
|
|
3
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, toonList, toonObject, touch, watchMail };
|
|
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 };
|
|
@@ -1,40 +1,10 @@
|
|
|
1
1
|
import { basename, dirname, join, resolve } from "node:path";
|
|
2
|
-
import { callerPane, currentPane, herdrMuxAdapter, nodeExec, probeMultiplexer, tmuxMuxAdapter } from "cyber-mux";
|
|
2
|
+
import { callerPane, currentPane, herdrMuxAdapter, nodeExec, nudge, probeMultiplexer, tmuxMuxAdapter } from "cyber-mux";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { assertDistinctFromPrimary, gitWorktreeAdapter, resolvePrimaryRoot } from "cyber-mux/worktree";
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
6
4
|
import { execFileSync } from "node:child_process";
|
|
7
5
|
import { homedir } from "node:os";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
|
|
11
|
-
* is still honored"). cyber-mux's own fast-path reads `$CYBER_MUX`/`$CYBER_MUX_PANE` — its
|
|
12
|
-
* `currentPane` hardcodes those names (no `envPrefix` override), and `probeMultiplexer`'s `envPrefix`
|
|
13
|
-
* option renames the WHOLE pair together, so neither can be steered to fall back onto a
|
|
14
|
-
* differently-named legacy pair. Every call into cyber-mux that reads the fast-path
|
|
15
|
-
* (`probeMultiplexer`, `currentPane`, `callerPane`, `resolveMuxAdapter`) must be handed this seam's
|
|
16
|
-
* output instead of the raw env.
|
|
17
|
-
*
|
|
18
|
-
* When `$CYBER_MUX`/`$CYBER_MUX_PANE` are both absent and either legacy `$CYBERLEGION_MUX`/
|
|
19
|
-
* `$CYBERLEGION_MUX_PANE` var is present, copies the legacy pair onto the current names. Never
|
|
20
|
-
* overwrites an already-set current var — the current pair always wins outright the moment either
|
|
21
|
-
* half of it is set, exactly matching the frozen precedence chain (mux.feature: "the current
|
|
22
|
-
* fast-path vars win over the legacy pair when both are set").
|
|
23
|
-
*
|
|
24
|
-
* Transitional — deleted once no pre-migration pane (one that only ever exported the legacy pair) is
|
|
25
|
-
* still alive.
|
|
26
|
-
*/
|
|
27
|
-
function normalizeMuxEnv(env) {
|
|
28
|
-
const hasCurrent = env.CYBER_MUX !== void 0 || env.CYBER_MUX_PANE !== void 0;
|
|
29
|
-
const hasLegacy = env.CYBERLEGION_MUX !== void 0 || env.CYBERLEGION_MUX_PANE !== void 0;
|
|
30
|
-
if (hasCurrent || !hasLegacy) return env;
|
|
31
|
-
return {
|
|
32
|
-
...env,
|
|
33
|
-
CYBER_MUX: env.CYBERLEGION_MUX,
|
|
34
|
-
CYBER_MUX_PANE: env.CYBERLEGION_MUX_PANE
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
//#endregion
|
|
6
|
+
import { assertDistinctFromPrimary, gitWorktreeAdapter, resolvePrimaryRoot } from "cyber-mux/worktree";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
38
8
|
//#region src/paths.ts
|
|
39
9
|
/** The tracked marker file that makes a hub root initialized (see ensureMarker). */
|
|
40
10
|
const MARKER_FILE = "config.json";
|
|
@@ -135,6 +105,149 @@ function sanitizePane(pane) {
|
|
|
135
105
|
return pane.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
136
106
|
}
|
|
137
107
|
//#endregion
|
|
108
|
+
//#region src/agentdef/resolve.ts
|
|
109
|
+
function parseAgentDefFile(text) {
|
|
110
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
|
|
111
|
+
if (!m) return {
|
|
112
|
+
fm: {},
|
|
113
|
+
instructions: text.trim()
|
|
114
|
+
};
|
|
115
|
+
const [, block, rest] = m;
|
|
116
|
+
const fm = {};
|
|
117
|
+
const lines = block.split("\n");
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < lines.length) {
|
|
120
|
+
const line = lines[i].replace(/\r$/, "");
|
|
121
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (line.length - line.trimStart().length > 0) {
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const idx = line.indexOf(":");
|
|
130
|
+
if (idx === -1) {
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const key = line.slice(0, idx).trim();
|
|
135
|
+
let value = line.slice(idx + 1).trim();
|
|
136
|
+
i++;
|
|
137
|
+
if (value === ">" || value === ">-" || value === "|" || value === "|-") {
|
|
138
|
+
const parts = [];
|
|
139
|
+
while (i < lines.length) {
|
|
140
|
+
const l = lines[i].replace(/\r$/, "");
|
|
141
|
+
if (l.trim() === "") {
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (l.length - l.trimStart().length === 0) break;
|
|
146
|
+
parts.push(l.trim());
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
value = parts.join(value.startsWith("|") ? "\n" : " ");
|
|
150
|
+
}
|
|
151
|
+
fm[key] = unquote(value);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
fm,
|
|
155
|
+
instructions: rest.trim()
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function unquote(v) {
|
|
159
|
+
if (v.length >= 2 && (v.startsWith("\"") && v.endsWith("\"") || v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1);
|
|
160
|
+
return v;
|
|
161
|
+
}
|
|
162
|
+
function toBool(v) {
|
|
163
|
+
if (v === void 0) return void 0;
|
|
164
|
+
if (v === "true") return true;
|
|
165
|
+
if (v === "false") return false;
|
|
166
|
+
}
|
|
167
|
+
function toAgentDef(path, text, fallbackName) {
|
|
168
|
+
const { fm, instructions } = parseAgentDefFile(text);
|
|
169
|
+
return {
|
|
170
|
+
name: fm.name ?? fallbackName,
|
|
171
|
+
description: fm.description,
|
|
172
|
+
model: fm.model,
|
|
173
|
+
effort: fm.effort,
|
|
174
|
+
harness: fm.harness,
|
|
175
|
+
warm: toBool(fm.warm),
|
|
176
|
+
interactive: toBool(fm.interactive),
|
|
177
|
+
instructions,
|
|
178
|
+
path
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The project's own `.agents/agents/` dir — the convention `agent list`/name-search scans. */
|
|
182
|
+
function projectAgentsDir(cwd) {
|
|
183
|
+
return join(projectRoot(cwd), ".agents", "agents");
|
|
184
|
+
}
|
|
185
|
+
/** Resolve one agent definition, by explicit file or by name search. Throws a clear error when
|
|
186
|
+
* neither locates a def — never returns a partial/empty result. */
|
|
187
|
+
function resolveAgentDef(input) {
|
|
188
|
+
if (input.file) {
|
|
189
|
+
const path = resolve(input.file);
|
|
190
|
+
if (!existsSync(path)) throw new Error(`agent def file "${input.file}" does not exist`);
|
|
191
|
+
return toAgentDef(path, readFileSync(path, "utf8"), basename(path).replace(/\.md$/, ""));
|
|
192
|
+
}
|
|
193
|
+
if (!input.name) throw new Error("resolveAgentDef needs a --agent <name> or --agent-file <path>");
|
|
194
|
+
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
195
|
+
for (const root of roots) {
|
|
196
|
+
const candidate = join(root, `${input.name}.md`);
|
|
197
|
+
if (existsSync(candidate)) return toAgentDef(candidate, readFileSync(candidate, "utf8"), input.name);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`no agent definition named "${input.name}" found under .agents/agents/`);
|
|
200
|
+
}
|
|
201
|
+
/** List every resolvable agent def under `.agents/agents/` (+ any extra search roots). Definitive
|
|
202
|
+
* empty array when the dir is absent or empty — never throws for "none found" here (unlike a
|
|
203
|
+
* named resolve, listing is inherently zero-or-more). */
|
|
204
|
+
function listAgentDefs(input = {}) {
|
|
205
|
+
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
206
|
+
const seen = /* @__PURE__ */ new Set();
|
|
207
|
+
const defs = [];
|
|
208
|
+
for (const root of roots) {
|
|
209
|
+
if (!existsSync(root)) continue;
|
|
210
|
+
for (const file of readdirSync(root)) {
|
|
211
|
+
if (!file.endsWith(".md")) continue;
|
|
212
|
+
const path = join(root, file);
|
|
213
|
+
if (seen.has(path)) continue;
|
|
214
|
+
seen.add(path);
|
|
215
|
+
defs.push(toAgentDef(path, readFileSync(path, "utf8"), file.replace(/\.md$/, "")));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return defs;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/mux-env.ts
|
|
222
|
+
/**
|
|
223
|
+
* Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
|
|
224
|
+
* is still honored"). cyber-mux's own fast-path reads `$CYBER_MUX`/`$CYBER_MUX_PANE` — its
|
|
225
|
+
* `currentPane` hardcodes those names (no `envPrefix` override), and `probeMultiplexer`'s `envPrefix`
|
|
226
|
+
* option renames the WHOLE pair together, so neither can be steered to fall back onto a
|
|
227
|
+
* differently-named legacy pair. Every call into cyber-mux that reads the fast-path
|
|
228
|
+
* (`probeMultiplexer`, `currentPane`, `callerPane`, `resolveMuxAdapter`) must be handed this seam's
|
|
229
|
+
* output instead of the raw env.
|
|
230
|
+
*
|
|
231
|
+
* When `$CYBER_MUX`/`$CYBER_MUX_PANE` are both absent and either legacy `$CYBERLEGION_MUX`/
|
|
232
|
+
* `$CYBERLEGION_MUX_PANE` var is present, copies the legacy pair onto the current names. Never
|
|
233
|
+
* overwrites an already-set current var — the current pair always wins outright the moment either
|
|
234
|
+
* half of it is set, exactly matching the frozen precedence chain (mux.feature: "the current
|
|
235
|
+
* fast-path vars win over the legacy pair when both are set").
|
|
236
|
+
*
|
|
237
|
+
* Transitional — deleted once no pre-migration pane (one that only ever exported the legacy pair) is
|
|
238
|
+
* still alive.
|
|
239
|
+
*/
|
|
240
|
+
function normalizeMuxEnv(env) {
|
|
241
|
+
const hasCurrent = env.CYBER_MUX !== void 0 || env.CYBER_MUX_PANE !== void 0;
|
|
242
|
+
const hasLegacy = env.CYBERLEGION_MUX !== void 0 || env.CYBERLEGION_MUX_PANE !== void 0;
|
|
243
|
+
if (hasCurrent || !hasLegacy) return env;
|
|
244
|
+
return {
|
|
245
|
+
...env,
|
|
246
|
+
CYBER_MUX: env.CYBERLEGION_MUX,
|
|
247
|
+
CYBER_MUX_PANE: env.CYBERLEGION_MUX_PANE
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
138
251
|
//#region src/identity.ts
|
|
139
252
|
const realExec = nodeExec;
|
|
140
253
|
/** `currentPane`, narrowed to a backend the registry can actually store a locator under. A caller
|
|
@@ -524,6 +637,125 @@ function prune(ctx) {
|
|
|
524
637
|
return changed;
|
|
525
638
|
}
|
|
526
639
|
//#endregion
|
|
640
|
+
//#region src/console/doorbell.ts
|
|
641
|
+
/** The doorbell text delivered to a woken recipient; also the standalone `unit nudge` default. */
|
|
642
|
+
const DELIVERY_DOORBELL = "You have unread mail — check your inbox.";
|
|
643
|
+
/**
|
|
644
|
+
* The first-turn doorbell `unit spawn` delivers to a freshly-opened paned peer — the instruction
|
|
645
|
+
* itself, not a notification that context is already populated. It names the brief's **file path**
|
|
646
|
+
* and tells the peer to read it and begin, so pickup does not depend on a SessionStart hook firing
|
|
647
|
+
* in the child (superseding ADR-0027, which split payload-delivery from turn-delivery).
|
|
648
|
+
*
|
|
649
|
+
* The path is named, never the brief's body: the brief is still written to its file and still never
|
|
650
|
+
* typed into the pane, so a long brief costs one line here however large it is, and a re-submit on
|
|
651
|
+
* the boot race re-types this instruction rather than the payload.
|
|
652
|
+
*/
|
|
653
|
+
function spawnDoorbell(briefPath) {
|
|
654
|
+
return `Read your brief at ${briefPath}, then begin work.`;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* A freshly-launched harness cold-boots slower than an already-running peer, so the spawn first-turn
|
|
658
|
+
* ring gets a wider retry budget than a plain `mail send` doorbell (nudge's own 10 × 400ms): flush the
|
|
659
|
+
* staged buffer for up to ~8s (20 × 400ms) before giving up. The common case still returns after one
|
|
660
|
+
* settle cycle once the peer takes the turn — the budget only bounds a slow or stuck boot. Still
|
|
661
|
+
* bounded — a harness that never reaches its prompt resolves to a best-effort warning, never a hang.
|
|
662
|
+
*/
|
|
663
|
+
const SPAWN_NUDGE_OPTS = {
|
|
664
|
+
attempts: 20,
|
|
665
|
+
settleMs: 400
|
|
666
|
+
};
|
|
667
|
+
/** Resolve an agent's live session pane: its recorded pane, else a pane pointer keyed by its id. */
|
|
668
|
+
function paneOf(store, id) {
|
|
669
|
+
return loadAgent(store, id)?.pane?.id ?? store.findPaneByAgentId(id);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Best-effort wake the recipient of a just-delivered message so it checks its inbox. A peer's live
|
|
673
|
+
* session pane, a standing owner's bound presence (`unit claim` — the live unit standing in for it,
|
|
674
|
+
* an exited one falling back below), or — with neither — the hub's bound main pane, is rung via the
|
|
675
|
+
* nudge submit-verify path (a taken turn, not fire-and-forget). Durable delivery already happened;
|
|
676
|
+
* the ring is opportunistic on top, so a legitimate no-op (`--no-nudge`, a headless/absent recipient
|
|
677
|
+
* with no live pane, a standing-owner send with no presence and no main pane bound, or a
|
|
678
|
+
* self-addressed send) rings nothing, and a ring that never completes within nudge's retry cap is
|
|
679
|
+
* swallowed into a warning. This never throws — it can never fail the send.
|
|
680
|
+
*
|
|
681
|
+
* The adapter is resolved lazily via `getAdapter`, invoked only once a pane to ring is confirmed and
|
|
682
|
+
* inside the same swallowing try — so a session with no mux backend (where `selectSessionAdapter`
|
|
683
|
+
* throws) is just a no-op wake, never a failed send.
|
|
684
|
+
*/
|
|
685
|
+
async function wakeRecipient(store, getAdapter, exec, input, nudgeOpts) {
|
|
686
|
+
if (input.noNudge) return { rung: false };
|
|
687
|
+
const recipient = loadAgent(store, input.toId);
|
|
688
|
+
if (!recipient) return { rung: false };
|
|
689
|
+
let pane;
|
|
690
|
+
let focusGated = false;
|
|
691
|
+
if (recipient.kind === "standing") {
|
|
692
|
+
const presenceUnit = presenceOf(store, recipient);
|
|
693
|
+
if (presenceUnit) pane = paneOf(store, presenceUnit.id);
|
|
694
|
+
else {
|
|
695
|
+
pane = store.getMainPane();
|
|
696
|
+
focusGated = true;
|
|
697
|
+
}
|
|
698
|
+
} else pane = paneOf(store, recipient.id);
|
|
699
|
+
if (!pane) return { rung: false };
|
|
700
|
+
if (pane === paneOf(store, input.fromId)) return { rung: false };
|
|
701
|
+
if (focusGated) {
|
|
702
|
+
let focused;
|
|
703
|
+
try {
|
|
704
|
+
focused = getAdapter().isPaneFocused(exec, { id: pane });
|
|
705
|
+
} catch {
|
|
706
|
+
focused = void 0;
|
|
707
|
+
}
|
|
708
|
+
if (focused === false) return {
|
|
709
|
+
rung: false,
|
|
710
|
+
pane
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
await nudge(getAdapter(), exec, { id: pane }, DELIVERY_DOORBELL, nudgeOpts);
|
|
715
|
+
return {
|
|
716
|
+
rung: true,
|
|
717
|
+
pane
|
|
718
|
+
};
|
|
719
|
+
} catch (err) {
|
|
720
|
+
return {
|
|
721
|
+
rung: false,
|
|
722
|
+
pane,
|
|
723
|
+
warning: err instanceof Error ? err.message : String(err)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Best-effort deliver a freshly-spawned paned peer's first turn so it acts on its brief with no
|
|
729
|
+
* human nudge. A paned agent boots to an idle prompt — the brief sits unread on disk and the model
|
|
730
|
+
* takes no turn on its own, unlike a subagent (where the caller's Task call IS the turn). So `unit
|
|
731
|
+
* spawn` rings the instruction (`spawnDoorbell`, naming the brief's file path) over the
|
|
732
|
+
* boot-race-aware `nudge` submit-verify path (a taken turn, never typing the brief itself), the same
|
|
733
|
+
* best-effort ring `wakeRecipient` gives `mail send`: the spawn (worktree, session, registry record)
|
|
734
|
+
* is the guaranteed effect and the ring is opportunistic on top, so `--no-wake` rings nothing and a
|
|
735
|
+
* ring that never completes within the retry budget is swallowed into a warning. This never throws —
|
|
736
|
+
* it can never fail the spawn.
|
|
737
|
+
*
|
|
738
|
+
* The adapter is resolved lazily via `getAdapter` inside the same swallowing try, so even a session
|
|
739
|
+
* whose backend has since gone away (where `selectSessionAdapter` would throw) degrades to a warned
|
|
740
|
+
* no-op rather than a failed spawn.
|
|
741
|
+
*/
|
|
742
|
+
async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS) {
|
|
743
|
+
if (input.noWake) return { rung: false };
|
|
744
|
+
try {
|
|
745
|
+
await nudge(getAdapter(), exec, input.target, spawnDoorbell(input.briefPath), nudgeOpts);
|
|
746
|
+
return {
|
|
747
|
+
rung: true,
|
|
748
|
+
pane: input.target.id
|
|
749
|
+
};
|
|
750
|
+
} catch (err) {
|
|
751
|
+
return {
|
|
752
|
+
rung: false,
|
|
753
|
+
pane: input.target.id,
|
|
754
|
+
warning: err instanceof Error ? err.message : String(err)
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
527
759
|
//#region src/mux-select.ts
|
|
528
760
|
/**
|
|
529
761
|
* Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
|
|
@@ -747,8 +979,10 @@ function resetCommandFor(harness) {
|
|
|
747
979
|
/**
|
|
748
980
|
* Launch a new peer session as a genuine sibling unit: create a real git worktree distinct from
|
|
749
981
|
* the primary checkout (refuse the primary checkout), open a session backend (tmux or herdr) with
|
|
750
|
-
* its cwd set to that worktree, pre-register the peer, and drop its brief as a file
|
|
751
|
-
*
|
|
982
|
+
* its cwd set to that worktree, pre-register the peer, and drop its brief as a file — never typed
|
|
983
|
+
* into its prompt. Nothing loads that file for the peer: it is the spawn wake that tells the peer to
|
|
984
|
+
* go read it, naming its path (`spawnAndWake` below, and ADR-0032). `spawn` alone therefore leaves a
|
|
985
|
+
* peer sitting idle with its brief unread, which is why callers want `spawnAndWake`.
|
|
752
986
|
*
|
|
753
987
|
* Spawned unit worktrees live sibling to the primary checkout (`<parent>/<repo>.worktrees/legion-<id6>`),
|
|
754
988
|
* never nested inside it, even though the registry/mailbox itself lives in the global hub — the hub
|
|
@@ -791,6 +1025,7 @@ function spawn(ctx, input) {
|
|
|
791
1025
|
const branch = input.branch ?? `cyberlegion/unit-${id}`;
|
|
792
1026
|
const at = input.at ?? "workspace";
|
|
793
1027
|
const worktreePath = input.worktreePath ?? resolveUnitWorktreePath(primaryRoot, id.slice(0, 6));
|
|
1028
|
+
assertDistinctFromPrimary(resolve(worktreePath), primaryRoot);
|
|
794
1029
|
if (at === "workspace" && sessionAdapter.worktree) {
|
|
795
1030
|
const opened = sessionAdapter.worktree.createInWorkspace(exec, {
|
|
796
1031
|
primaryRoot,
|
|
@@ -835,7 +1070,7 @@ function spawn(ctx, input) {
|
|
|
835
1070
|
mux: muxName,
|
|
836
1071
|
id: target.id
|
|
837
1072
|
} : null,
|
|
838
|
-
status: "
|
|
1073
|
+
status: "active",
|
|
839
1074
|
createdAt: ts,
|
|
840
1075
|
lastSeen: ts,
|
|
841
1076
|
brief: paths.briefFile(ctx.store.root, id),
|
|
@@ -851,6 +1086,35 @@ function spawn(ctx, input) {
|
|
|
851
1086
|
};
|
|
852
1087
|
}
|
|
853
1088
|
/**
|
|
1089
|
+
* `unit spawn` end to end: open the peer, then deliver its first turn.
|
|
1090
|
+
*
|
|
1091
|
+
* The two acts live together here rather than at the CLI call site so the **brief path the doorbell
|
|
1092
|
+
* names is derived from the record `spawn` just wrote**, not handed in by a caller. That is the
|
|
1093
|
+
* point of the seam: a caller cannot ring with the wrong path because it supplies no path — an
|
|
1094
|
+
* empty string, a stale path, or the brief's whole body are all unrepresentable here, rather than
|
|
1095
|
+
* merely untested.
|
|
1096
|
+
*
|
|
1097
|
+
* The ring is best-effort on top of the guaranteed spawn effect (worktree + session + registry
|
|
1098
|
+
* record): `--no-wake` rings nothing, and a ring that never completes is reported as a warning on
|
|
1099
|
+
* the result rather than thrown, so it can never fail a spawn that already landed.
|
|
1100
|
+
*/
|
|
1101
|
+
async function spawnAndWake(ctx, input, options = {}) {
|
|
1102
|
+
const res = spawn(ctx, input);
|
|
1103
|
+
const env = ctx.env ?? process.env;
|
|
1104
|
+
const exec = ctx.exec ?? realExec;
|
|
1105
|
+
const briefPath = res.agent.brief;
|
|
1106
|
+
const wake = await wakeSpawn(() => selectSessionAdapter(env, exec), exec, {
|
|
1107
|
+
target: { id: res.pane },
|
|
1108
|
+
briefPath: briefPath ?? "",
|
|
1109
|
+
noWake: options.noWake || !briefPath
|
|
1110
|
+
}, options.nudgeOpts);
|
|
1111
|
+
return {
|
|
1112
|
+
...res,
|
|
1113
|
+
rung: wake.rung,
|
|
1114
|
+
...wake.warning ? { warning: wake.warning } : {}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
854
1118
|
* The label a `workspace` placement opens under — the short human-scannable name that makes a unit's
|
|
855
1119
|
* own visible space findable by eye (`console/workspace-label.ts` owns the naming rule). Only the
|
|
856
1120
|
* `workspace` placement carries one: a pane or tab lives inside a space the caller is already
|
|
@@ -882,6 +1146,67 @@ function muxEnvPrefix(muxName) {
|
|
|
882
1146
|
return "";
|
|
883
1147
|
}
|
|
884
1148
|
/**
|
|
1149
|
+
* Resolve a unit's live session pane from a ref, or throw naming the ref. The one place
|
|
1150
|
+
* focus/nudge/read agree on what "addressable" means — a record's own pane, else a pane pointer
|
|
1151
|
+
* keyed by its id (a herdr peer stores its pane only in that index).
|
|
1152
|
+
*/
|
|
1153
|
+
function paneTargetOf(ctx, ref) {
|
|
1154
|
+
const agent = resolveAgent(ctx.store, ref);
|
|
1155
|
+
const pane = agent.pane?.id ?? ctx.store.findPaneByAgentId(agent.id);
|
|
1156
|
+
if (!pane) throw new Error(`unit "${ref}" has no known session pane`);
|
|
1157
|
+
return {
|
|
1158
|
+
agent,
|
|
1159
|
+
target: { id: pane }
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Move input focus to a peer's session. The adapter beams the attached view all the way there —
|
|
1164
|
+
* across workspace and tab — rather than no-opping when the peer sits outside the caller's current
|
|
1165
|
+
* workspace, and surfaces a backend failure instead of reporting a false success.
|
|
1166
|
+
*
|
|
1167
|
+
* Lives here rather than inline in the CLI for the same reason `spawnAndWake` does: composed at the
|
|
1168
|
+
* call site it took a hardcoded `realExec`, so the operation could not be driven with an injected
|
|
1169
|
+
* exec and its success path was unreachable from a test.
|
|
1170
|
+
*/
|
|
1171
|
+
function focusUnit(ctx, ref) {
|
|
1172
|
+
const { agent, target } = paneTargetOf(ctx, ref);
|
|
1173
|
+
const exec = ctx.exec ?? realExec;
|
|
1174
|
+
selectSessionAdapter(ctx.env ?? process.env, exec).focus(exec, target);
|
|
1175
|
+
return {
|
|
1176
|
+
agent,
|
|
1177
|
+
pane: target.id
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Ring a peer's session as a **taken turn**, not fire-and-forget: `nudge` submits, reads the pane
|
|
1182
|
+
* back to confirm the text is no longer staged, and flushes the staged buffer (never re-typing) up
|
|
1183
|
+
* to a bounded cap — then throws if the peer never took the turn. A doorbell must carry text; an
|
|
1184
|
+
* empty ring is a no-op, so the default points the peer at its inbox.
|
|
1185
|
+
*/
|
|
1186
|
+
async function nudgeUnit(ctx, ref, options = {}) {
|
|
1187
|
+
const { agent, target } = paneTargetOf(ctx, ref);
|
|
1188
|
+
const exec = ctx.exec ?? realExec;
|
|
1189
|
+
const message = options.message || "You have unread mail — check your inbox.";
|
|
1190
|
+
const result = await nudge(selectSessionAdapter(ctx.env ?? process.env, exec), exec, target, message, options.nudgeOpts);
|
|
1191
|
+
return {
|
|
1192
|
+
agent,
|
|
1193
|
+
pane: target.id,
|
|
1194
|
+
message,
|
|
1195
|
+
resubmits: result.resubmits
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
/** Scrape the trailing output of a peer's session screen. */
|
|
1199
|
+
function readUnit(ctx, ref, options = {}) {
|
|
1200
|
+
const { agent, target } = paneTargetOf(ctx, ref);
|
|
1201
|
+
const exec = ctx.exec ?? realExec;
|
|
1202
|
+
const output = selectSessionAdapter(ctx.env ?? process.env, exec).read(exec, target, { lines: options.lines });
|
|
1203
|
+
return {
|
|
1204
|
+
agent,
|
|
1205
|
+
pane: target.id,
|
|
1206
|
+
output
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
885
1210
|
* Reset a warm peer's context to cold WITHOUT tearing anything down — injects the peer's own
|
|
886
1211
|
* harness fresh-context command (`resetCommandFor`) into its pane through the session adapter.
|
|
887
1212
|
* Warmth is the unit (pane/process stays warm — no cold-start), coldness is the context. The
|
|
@@ -890,9 +1215,8 @@ function muxEnvPrefix(muxName) {
|
|
|
890
1215
|
* nor the worktree — `close` (`decommission`) owns teardown.
|
|
891
1216
|
*/
|
|
892
1217
|
function clearUnit(ctx, ref) {
|
|
893
|
-
const agent =
|
|
894
|
-
const pane =
|
|
895
|
-
if (!pane) throw new Error(`unit "${ref}" has no known session pane`);
|
|
1218
|
+
const { agent, target } = paneTargetOf(ctx, ref);
|
|
1219
|
+
const pane = target.id;
|
|
896
1220
|
if (!agent.harness) throw new Error(`unit "${ref}" has no harness on record — cannot resolve its reset command`);
|
|
897
1221
|
const command = resetCommandFor(agent.harness);
|
|
898
1222
|
const env = ctx.env ?? process.env;
|
|
@@ -933,119 +1257,28 @@ function realizeLaunch(def, opts = {}) {
|
|
|
933
1257
|
command: parts.join(" ")
|
|
934
1258
|
};
|
|
935
1259
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
if (line.length - line.trimStart().length > 0) {
|
|
955
|
-
i++;
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
const idx = line.indexOf(":");
|
|
959
|
-
if (idx === -1) {
|
|
960
|
-
i++;
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
const key = line.slice(0, idx).trim();
|
|
964
|
-
let value = line.slice(idx + 1).trim();
|
|
965
|
-
i++;
|
|
966
|
-
if (value === ">" || value === ">-" || value === "|" || value === "|-") {
|
|
967
|
-
const parts = [];
|
|
968
|
-
while (i < lines.length) {
|
|
969
|
-
const l = lines[i].replace(/\r$/, "");
|
|
970
|
-
if (l.trim() === "") {
|
|
971
|
-
i++;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
if (l.length - l.trimStart().length === 0) break;
|
|
975
|
-
parts.push(l.trim());
|
|
976
|
-
i++;
|
|
977
|
-
}
|
|
978
|
-
value = parts.join(value.startsWith("|") ? "\n" : " ");
|
|
979
|
-
}
|
|
980
|
-
fm[key] = unquote(value);
|
|
981
|
-
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Resolve what `unit spawn` should launch, from either an explicit `--harness` or an agent def
|
|
1262
|
+
* (`--agent` / `--agent-file`) whose harness, model and instructions compose the launch command.
|
|
1263
|
+
* An explicit `--harness` overrides the def's own.
|
|
1264
|
+
*
|
|
1265
|
+
* Extracted from the CLI action so the def→launch wiring is reachable from a test: composed inline
|
|
1266
|
+
* it sat between two well-covered halves (`resolveAgentDef`, `realizeLaunch`) with nothing
|
|
1267
|
+
* exercising the join between them.
|
|
1268
|
+
*/
|
|
1269
|
+
function resolveSpawnLaunch(input) {
|
|
1270
|
+
if (!input.agent && !input.agentFile) return { harness: input.harness };
|
|
1271
|
+
const realized = realizeLaunch(resolveAgentDef({
|
|
1272
|
+
name: input.agent,
|
|
1273
|
+
file: input.agentFile,
|
|
1274
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
1275
|
+
...input.searchRoots ? { searchRoots: input.searchRoots } : {}
|
|
1276
|
+
}), { harness: input.harness });
|
|
982
1277
|
return {
|
|
983
|
-
|
|
984
|
-
|
|
1278
|
+
harness: realized.harness,
|
|
1279
|
+
command: realized.command
|
|
985
1280
|
};
|
|
986
1281
|
}
|
|
987
|
-
function unquote(v) {
|
|
988
|
-
if (v.length >= 2 && (v.startsWith("\"") && v.endsWith("\"") || v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1);
|
|
989
|
-
return v;
|
|
990
|
-
}
|
|
991
|
-
function toBool(v) {
|
|
992
|
-
if (v === void 0) return void 0;
|
|
993
|
-
if (v === "true") return true;
|
|
994
|
-
if (v === "false") return false;
|
|
995
|
-
}
|
|
996
|
-
function toAgentDef(path, text, fallbackName) {
|
|
997
|
-
const { fm, instructions } = parseAgentDefFile(text);
|
|
998
|
-
return {
|
|
999
|
-
name: fm.name ?? fallbackName,
|
|
1000
|
-
description: fm.description,
|
|
1001
|
-
model: fm.model,
|
|
1002
|
-
effort: fm.effort,
|
|
1003
|
-
harness: fm.harness,
|
|
1004
|
-
warm: toBool(fm.warm),
|
|
1005
|
-
interactive: toBool(fm.interactive),
|
|
1006
|
-
instructions,
|
|
1007
|
-
path
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
/** The project's own `.agents/agents/` dir — the convention `agent list`/name-search scans. */
|
|
1011
|
-
function projectAgentsDir(cwd) {
|
|
1012
|
-
return join(projectRoot(cwd), ".agents", "agents");
|
|
1013
|
-
}
|
|
1014
|
-
/** Resolve one agent definition, by explicit file or by name search. Throws a clear error when
|
|
1015
|
-
* neither locates a def — never returns a partial/empty result. */
|
|
1016
|
-
function resolveAgentDef(input) {
|
|
1017
|
-
if (input.file) {
|
|
1018
|
-
const path = resolve(input.file);
|
|
1019
|
-
if (!existsSync(path)) throw new Error(`agent def file "${input.file}" does not exist`);
|
|
1020
|
-
return toAgentDef(path, readFileSync(path, "utf8"), basename(path).replace(/\.md$/, ""));
|
|
1021
|
-
}
|
|
1022
|
-
if (!input.name) throw new Error("resolveAgentDef needs a --agent <name> or --agent-file <path>");
|
|
1023
|
-
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
1024
|
-
for (const root of roots) {
|
|
1025
|
-
const candidate = join(root, `${input.name}.md`);
|
|
1026
|
-
if (existsSync(candidate)) return toAgentDef(candidate, readFileSync(candidate, "utf8"), input.name);
|
|
1027
|
-
}
|
|
1028
|
-
throw new Error(`no agent definition named "${input.name}" found under .agents/agents/`);
|
|
1029
|
-
}
|
|
1030
|
-
/** List every resolvable agent def under `.agents/agents/` (+ any extra search roots). Definitive
|
|
1031
|
-
* empty array when the dir is absent or empty — never throws for "none found" here (unlike a
|
|
1032
|
-
* named resolve, listing is inherently zero-or-more). */
|
|
1033
|
-
function listAgentDefs(input = {}) {
|
|
1034
|
-
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
1035
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1036
|
-
const defs = [];
|
|
1037
|
-
for (const root of roots) {
|
|
1038
|
-
if (!existsSync(root)) continue;
|
|
1039
|
-
for (const file of readdirSync(root)) {
|
|
1040
|
-
if (!file.endsWith(".md")) continue;
|
|
1041
|
-
const path = join(root, file);
|
|
1042
|
-
if (seen.has(path)) continue;
|
|
1043
|
-
seen.add(path);
|
|
1044
|
-
defs.push(toAgentDef(path, readFileSync(path, "utf8"), file.replace(/\.md$/, "")));
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return defs;
|
|
1048
|
-
}
|
|
1049
1282
|
//#endregion
|
|
1050
1283
|
//#region src/decommission.ts
|
|
1051
1284
|
/**
|
|
@@ -1303,9 +1536,13 @@ function fail(msg) {
|
|
|
1303
1536
|
//#region src/runtime/inject-inbox.ts
|
|
1304
1537
|
const EVENTS = ["SessionStart", "PostToolUse"];
|
|
1305
1538
|
/**
|
|
1306
|
-
* Resolve the calling agent, gather its
|
|
1307
|
-
* SessionStart-style injection payload — or null when there is
|
|
1308
|
-
* unregistered caller or an empty inbox never fails the harness hook).
|
|
1539
|
+
* Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
|
|
1540
|
+
* the hub's main pane), and return the SessionStart-style injection payload — or null when there is
|
|
1541
|
+
* nothing to inject (an unregistered caller or an empty inbox never fails the harness hook).
|
|
1542
|
+
*
|
|
1543
|
+
* This never injects a brief. Brief delivery is the spawn wake's job: the first-turn doorbell
|
|
1544
|
+
* carries the instruction and names the brief's file path, so pickup does not depend on this hook
|
|
1545
|
+
* firing in the child (`unit/lifecycle`, superseding ADR-0027).
|
|
1309
1546
|
*/
|
|
1310
1547
|
function injectInbox(ctx, event) {
|
|
1311
1548
|
if (!EVENTS.includes(event)) throw new Error(`unsupported --event "${event}" (expected ${EVENTS.join(" | ")})`);
|
|
@@ -1321,12 +1558,6 @@ function injectInbox(ctx, event) {
|
|
|
1321
1558
|
}
|
|
1322
1559
|
const parts = [];
|
|
1323
1560
|
const rec = loadAgent(ctx.store, meId);
|
|
1324
|
-
if (rec?.status === "spawning") {
|
|
1325
|
-
const brief = ctx.store.readBrief(meId);
|
|
1326
|
-
if (brief) parts.push(`## Your brief\n\n${brief.trim()}`);
|
|
1327
|
-
rec.status = "active";
|
|
1328
|
-
saveAgent(ctx.store, rec);
|
|
1329
|
-
}
|
|
1330
1561
|
const unread = inbox({ store: ctx.store }, {
|
|
1331
1562
|
meId,
|
|
1332
1563
|
unread: true
|
|
@@ -1557,4 +1788,4 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
|
|
|
1557
1788
|
}
|
|
1558
1789
|
}
|
|
1559
1790
|
//#endregion
|
|
1560
|
-
export {
|
|
1791
|
+
export { resolveProjectLocalRoot as $, bumpLastSeen as A, registerStanding as B, nudgeUnit as C, selectSessionAdapter as D, spawnAndWake as E, loadAgent as F, resolveStandingOwner as G, resolvePresence as H, prune as I, listAgentDefs as J, touch as K, realExec as L, clearPresence as M, detectHarness as N, DELIVERY_DOORBELL as O, listAgents as P, projectRoot as Q, reconcile as R, focusUnit as S, spawn as T, resolveRecipient as U, resolveAgent as V, resolveSelfId as W, ensureMarker as X, resolveAgentDef as Y, paths as Z, install as _, emit as a, resolveSpawnLaunch as b, toonList as c, deleteMessage as d, resolveRoot as et, inbox as f, send as g, resolveBody as h, injectInbox as i, claimPresence as j, wakeRecipient as k, toonObject as l, readAck as m, awaitReply as n, fail as o, peek as p, normalizeMuxEnv as q, FileStore as r, nextStep as s, watchMail as t, ack as u, decommission as v, readUnit as w, clearUnit as x, realizeLaunch as y, register as z };
|