cyberlegion 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +399 -193
- package/dist/index.d.mts +75 -262
- package/dist/index.mjs +4 -28
- package/dist/watch-nkm6F1_f.mjs +1560 -0
- package/package.json +7 -5
- package/readme.md +35 -30
- package/dist/watch-DA8I44dv.mjs +0 -1403
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as loadAgent, B as resolveSelfId, C as spawn, D as clearPresence, E as claimPresence, F as register, H as touch, I as registerStanding, J as normalizeMuxEnv, L as resolveAgent, M as prune, N as realExec, O as detectHarness, P as reconcile, R as resolvePresence, S as clearUnit, T as bumpLastSeen, V as resolveStandingOwner, _ as install, a as emit, b as resolveAgentDef, c as toonList, d as deleteMessage, f as inbox, g as send, h as resolveBody, i as injectInbox, j as presenceOf, k as listAgents, l as toonObject, m as readAck, n as awaitReply, o as fail, p as peek, q as resolveRoot, r as FileStore, s as nextStep, t as watchMail, u as ack, v as decommission, w as selectSessionAdapter, x as realizeLaunch, y as listAgentDefs } from "./watch-nkm6F1_f.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
|
+
import { currentPane, nudge, probeMultiplexer } from "cyber-mux";
|
|
5
6
|
//#region src/admin.ts
|
|
6
7
|
/**
|
|
7
8
|
* Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
|
|
@@ -37,6 +38,119 @@ function migrateStore(from, to) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
//#endregion
|
|
41
|
+
//#region src/console/doorbell.ts
|
|
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.";
|
|
44
|
+
/**
|
|
45
|
+
* The first-turn doorbell `unit spawn` delivers to a freshly-opened paned peer. The peer's brief is
|
|
46
|
+
* already injected into its context by its own SessionStart hook (never re-typed here — the ring
|
|
47
|
+
* only makes the peer take a turn); this text points it at that loaded brief so it starts on its own
|
|
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.
|
|
75
|
+
*
|
|
76
|
+
* The adapter is resolved lazily via `getAdapter`, invoked only once a pane to ring is confirmed and
|
|
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.
|
|
79
|
+
*/
|
|
80
|
+
async function wakeRecipient(store, getAdapter, exec, input, nudgeOpts) {
|
|
81
|
+
if (input.noNudge) return { rung: false };
|
|
82
|
+
const recipient = loadAgent(store, input.toId);
|
|
83
|
+
if (!recipient) return { rung: false };
|
|
84
|
+
let pane;
|
|
85
|
+
let focusGated = false;
|
|
86
|
+
if (recipient.kind === "standing") {
|
|
87
|
+
const presenceUnit = presenceOf(store, recipient);
|
|
88
|
+
if (presenceUnit) pane = paneOf(store, presenceUnit.id);
|
|
89
|
+
else {
|
|
90
|
+
pane = store.getMainPane();
|
|
91
|
+
focusGated = true;
|
|
92
|
+
}
|
|
93
|
+
} else pane = paneOf(store, recipient.id);
|
|
94
|
+
if (!pane) return { rung: false };
|
|
95
|
+
if (pane === paneOf(store, input.fromId)) return { rung: false };
|
|
96
|
+
if (focusGated) {
|
|
97
|
+
let focused;
|
|
98
|
+
try {
|
|
99
|
+
focused = getAdapter().isPaneFocused(exec, { id: pane });
|
|
100
|
+
} catch {
|
|
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
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Best-effort deliver a freshly-spawned paned peer's first turn so it acts on its already-loaded
|
|
124
|
+
* brief with no human nudge. A paned agent boots to an idle prompt — its brief is injected into
|
|
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
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
40
154
|
//#region src/cli.ts
|
|
41
155
|
const VERSION = "0.0.0";
|
|
42
156
|
function ctxOf(opts) {
|
|
@@ -50,13 +164,13 @@ function formatOf(opts) {
|
|
|
50
164
|
}
|
|
51
165
|
function requireSelf(ctx) {
|
|
52
166
|
const id = resolveSelfId(ctx);
|
|
53
|
-
if (!id) fail("no identity in this session — run `cyberlegion
|
|
167
|
+
if (!id) fail("no identity in this session — run `cyberlegion unit register` first");
|
|
54
168
|
bumpLastSeen(ctx, id);
|
|
55
169
|
return id;
|
|
56
170
|
}
|
|
57
171
|
function resolveTarget(ctx, ref) {
|
|
58
172
|
const agent = resolveAgent(ctx.store, ref);
|
|
59
|
-
const pane = agent.
|
|
173
|
+
const pane = agent.pane?.id ?? ctx.store.findPaneByAgentId(agent.id);
|
|
60
174
|
if (!pane) fail(`unit "${ref}" has no known session pane`);
|
|
61
175
|
return { id: pane };
|
|
62
176
|
}
|
|
@@ -64,10 +178,55 @@ function withGlobals(cmd) {
|
|
|
64
178
|
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"));
|
|
65
179
|
}
|
|
66
180
|
const program = new Command();
|
|
67
|
-
program.name("cyberlegion").description("Harness-agnostic agent session spawning
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
181
|
+
program.name("cyberlegion").description("Harness-agnostic agent session spawning and messaging over the filesystem").version(VERSION).enablePositionalOptions();
|
|
182
|
+
const unit = program.command("unit").description("legion units — register, discover, spawn, and reap");
|
|
183
|
+
/** The standing-owner branch of `unit register --standing` (folds the old `identity owner`). */
|
|
184
|
+
function runStanding(ctx, opts) {
|
|
185
|
+
if (!opts.handle) {
|
|
186
|
+
const standing = listAgents(ctx.store).filter((a) => a.kind === "standing");
|
|
187
|
+
emit(formatOf(opts), {
|
|
188
|
+
toon: toonList("agents", standing, [
|
|
189
|
+
{
|
|
190
|
+
key: "id",
|
|
191
|
+
get: (a) => a.id
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
key: "handle",
|
|
195
|
+
get: (a) => a.handle
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
key: "harness",
|
|
199
|
+
get: (a) => a.harness ?? "-"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "status",
|
|
203
|
+
get: (a) => a.status
|
|
204
|
+
}
|
|
205
|
+
], `${standing.length} standing`),
|
|
206
|
+
json: standing
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const liveClaim = listAgents(ctx.store).find((a) => a.handle === opts.handle && a.kind !== "standing" && a.status !== "exited");
|
|
211
|
+
const rec = registerStanding(ctx, { handle: opts.handle });
|
|
212
|
+
if (liveClaim) console.error(`a live session already claims handle "${opts.handle}"`);
|
|
213
|
+
emit(formatOf(opts), {
|
|
214
|
+
toon: toonObject({
|
|
215
|
+
id: rec.id,
|
|
216
|
+
handle: rec.handle,
|
|
217
|
+
kind: rec.kind,
|
|
218
|
+
status: rec.status
|
|
219
|
+
}),
|
|
220
|
+
json: rec
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
withGlobals(unit.command("register")).description("register or refresh this session identity (or --standing: a session-independent owner inbox)").option("--handle <name>", "human handle for this agent").option("--harness <h>", "claude | cursor | codex (else auto-detected)").option("--standing", "mint a standing, session-independent owner inbox (bare, with no --handle: list them)").action((opts) => {
|
|
224
|
+
const ctx = ctxOf(opts);
|
|
225
|
+
if (opts.standing) {
|
|
226
|
+
runStanding(ctx, opts);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const rec = register(ctx, {
|
|
71
230
|
handle: opts.handle,
|
|
72
231
|
harness: opts.harness
|
|
73
232
|
});
|
|
@@ -75,23 +234,67 @@ withGlobals(identity.command("register")).description("register or refresh this
|
|
|
75
234
|
toon: toonObject({
|
|
76
235
|
id: rec.id,
|
|
77
236
|
handle: rec.handle,
|
|
78
|
-
harness: rec.harness,
|
|
237
|
+
harness: rec.harness ?? "-",
|
|
79
238
|
status: rec.status
|
|
80
239
|
}),
|
|
81
240
|
json: rec
|
|
82
241
|
});
|
|
83
242
|
});
|
|
84
|
-
withGlobals(
|
|
243
|
+
withGlobals(unit.command("claim")).description("bind the caller's unit as a standing owner's presence; --show reads it, --clear unbinds").argument("<handle>", "standing owner handle").option("--clear", "unbind the presence (a no-op when nothing is bound)").option("--show", "print the bound presence instead of claiming").action((handle, opts) => {
|
|
244
|
+
const ctx = ctxOf(opts);
|
|
245
|
+
if (opts.show) {
|
|
246
|
+
let presence;
|
|
247
|
+
try {
|
|
248
|
+
presence = resolvePresence(ctx.store, handle);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
251
|
+
}
|
|
252
|
+
emit(formatOf(opts), {
|
|
253
|
+
toon: toonObject({ presence: presence?.id ?? "none" }),
|
|
254
|
+
json: { presence: presence?.id ?? null }
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (opts.clear) {
|
|
259
|
+
try {
|
|
260
|
+
clearPresence(ctx, handle);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
263
|
+
}
|
|
264
|
+
emit(formatOf(opts), {
|
|
265
|
+
toon: toonObject({ presence: "none" }),
|
|
266
|
+
json: { presence: null }
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let rec;
|
|
271
|
+
try {
|
|
272
|
+
rec = claimPresence(ctx, handle);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
275
|
+
}
|
|
276
|
+
emit(formatOf(opts), {
|
|
277
|
+
toon: toonObject({
|
|
278
|
+
owner: rec.handle,
|
|
279
|
+
presence: rec.presence
|
|
280
|
+
}),
|
|
281
|
+
json: {
|
|
282
|
+
owner: rec.handle,
|
|
283
|
+
presence: rec.presence
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
withGlobals(unit.command("whoami")).description("print this session own identity").action((opts) => {
|
|
85
288
|
const ctx = ctxOf(opts);
|
|
86
289
|
const id = resolveSelfId(ctx);
|
|
87
|
-
if (!id) fail("no identity in this session — run `cyberlegion
|
|
290
|
+
if (!id) fail("no identity in this session — run `cyberlegion unit register` first");
|
|
88
291
|
const rec = loadAgent(ctx.store, id);
|
|
89
292
|
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
90
293
|
emit(formatOf(opts), {
|
|
91
294
|
toon: toonObject({
|
|
92
295
|
id: rec.id,
|
|
93
296
|
handle: rec.handle,
|
|
94
|
-
harness: rec.harness,
|
|
297
|
+
harness: rec.harness ?? "-",
|
|
95
298
|
status: rec.status
|
|
96
299
|
}),
|
|
97
300
|
json: rec
|
|
@@ -100,9 +303,10 @@ withGlobals(identity.command("whoami")).description("print this session own iden
|
|
|
100
303
|
function runWho(opts) {
|
|
101
304
|
const ctx = ctxOf(opts);
|
|
102
305
|
touch(ctx);
|
|
306
|
+
if (opts.reconcile) reconcile(ctx, { adopt: true });
|
|
103
307
|
const agents = listAgents(ctx.store).filter((a) => opts.all || a.status !== "exited");
|
|
104
308
|
emit(formatOf(opts), {
|
|
105
|
-
toon: toonList("
|
|
309
|
+
toon: toonList("units", agents, [
|
|
106
310
|
{
|
|
107
311
|
key: "id",
|
|
108
312
|
get: (a) => a.id
|
|
@@ -113,19 +317,23 @@ function runWho(opts) {
|
|
|
113
317
|
},
|
|
114
318
|
{
|
|
115
319
|
key: "harness",
|
|
116
|
-
get: (a) => a.harness
|
|
320
|
+
get: (a) => a.harness ?? "-"
|
|
117
321
|
},
|
|
118
322
|
{
|
|
119
323
|
key: "status",
|
|
120
324
|
get: (a) => a.status
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
key: "pane",
|
|
328
|
+
get: (a) => a.pane?.id ?? "-"
|
|
121
329
|
}
|
|
122
|
-
], `${agents.length}
|
|
330
|
+
], `${agents.length} units`),
|
|
123
331
|
json: agents
|
|
124
332
|
});
|
|
125
|
-
if (agents.length === 0) nextStep("cyberlegion
|
|
333
|
+
if (agents.length === 0) nextStep("cyberlegion unit register to join");
|
|
126
334
|
}
|
|
127
|
-
withGlobals(
|
|
128
|
-
withGlobals(
|
|
335
|
+
withGlobals(unit.command("who")).description("list the addressable units").option("--all", "include exited units").option("--reconcile", "live-probe the current mux: cull dead-pane records and adopt unbound harness-bearing panes before listing").action(runWho);
|
|
336
|
+
withGlobals(unit.command("prune")).description("mark dead units exited and sweep").action((opts) => {
|
|
129
337
|
const ctx = ctxOf(opts);
|
|
130
338
|
touch(ctx);
|
|
131
339
|
const changed = prune(ctx);
|
|
@@ -140,14 +348,13 @@ withGlobals(identity.command("prune")).description("mark dead agents exited and
|
|
|
140
348
|
json: changed
|
|
141
349
|
});
|
|
142
350
|
});
|
|
143
|
-
const session = program.command("session").description("warm peer session lifecycle over a multiplexer");
|
|
144
351
|
function defineSpawn(cmd) {
|
|
145
|
-
return withGlobals(cmd).description("launch a new peer session in its own git worktree (tmux or herdr)").option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
352
|
+
return withGlobals(cmd).description("launch a new peer session in its own git worktree (tmux or herdr)").option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").option("--cwd <path>", "spawn the session in an existing directory; create no worktree (mutually exclusive with --branch/--worktree-path)").addOption(new Option("--at <placement>", "where to open the new session (default: new-worktree → workspace, --cwd → tab)").choices([
|
|
146
353
|
"pane:right",
|
|
147
354
|
"pane:down",
|
|
148
355
|
"tab",
|
|
149
|
-
"
|
|
150
|
-
]).
|
|
356
|
+
"workspace"
|
|
357
|
+
])).option("--no-wake", "suppress the first-turn doorbell (spawn idle; the caller drives the first turn itself)").action(async (opts) => {
|
|
151
358
|
const ctx = ctxOf(opts);
|
|
152
359
|
touch(ctx);
|
|
153
360
|
let harness = opts.harness;
|
|
@@ -166,7 +373,7 @@ function defineSpawn(cmd) {
|
|
|
166
373
|
harness = realized.harness;
|
|
167
374
|
command = realized.command;
|
|
168
375
|
}
|
|
169
|
-
if (!harness) fail("
|
|
376
|
+
if (!harness) fail("unit spawn needs --harness, or --agent/--agent-file resolving one");
|
|
170
377
|
const res = spawn(ctx, {
|
|
171
378
|
harness,
|
|
172
379
|
command,
|
|
@@ -175,23 +382,33 @@ function defineSpawn(cmd) {
|
|
|
175
382
|
handle: opts.handle,
|
|
176
383
|
branch: opts.branch,
|
|
177
384
|
worktreePath: opts.worktreePath,
|
|
385
|
+
cwd: opts.cwd,
|
|
178
386
|
at: opts.at
|
|
179
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}`);
|
|
180
393
|
emit(formatOf(opts), {
|
|
181
394
|
toon: toonObject({
|
|
182
395
|
spawned: res.agent.id,
|
|
183
396
|
handle: res.agent.handle,
|
|
184
397
|
harness: res.agent.harness,
|
|
185
398
|
worktree: res.agent.worktree?.root,
|
|
186
|
-
pane: res.pane
|
|
399
|
+
pane: res.pane,
|
|
400
|
+
rung: wake.rung
|
|
187
401
|
}),
|
|
188
|
-
json:
|
|
402
|
+
json: {
|
|
403
|
+
...res,
|
|
404
|
+
rung: wake.rung
|
|
405
|
+
}
|
|
189
406
|
});
|
|
190
|
-
nextStep(`cyberlegion
|
|
407
|
+
nextStep(`cyberlegion unit read ${res.agent.id}`);
|
|
191
408
|
});
|
|
192
409
|
}
|
|
193
|
-
defineSpawn(
|
|
194
|
-
withGlobals(
|
|
410
|
+
defineSpawn(unit.command("spawn"));
|
|
411
|
+
withGlobals(unit.command("close")).description("tear down a unit's worktree + session and reap its state (the inverse of spawn)").argument("<id>", "unit id, handle, or worktree branch/CR ref").option("--force", "discard uncommitted changes in the worktree (never overrides refusing the primary checkout)").action((ref, opts) => {
|
|
195
412
|
const ctx = ctxOf(opts);
|
|
196
413
|
touch(ctx);
|
|
197
414
|
const agent = resolveAgent(ctx.store, ref);
|
|
@@ -208,33 +425,7 @@ withGlobals(session.command("close")).description("tear down a unit's worktree +
|
|
|
208
425
|
json: res
|
|
209
426
|
});
|
|
210
427
|
});
|
|
211
|
-
withGlobals(
|
|
212
|
-
const ctx = ctxOf(opts);
|
|
213
|
-
touch(ctx);
|
|
214
|
-
const agents = listAgents(ctx.store).filter((a) => a.status !== "exited");
|
|
215
|
-
emit(formatOf(opts), {
|
|
216
|
-
toon: toonList("sessions", agents, [
|
|
217
|
-
{
|
|
218
|
-
key: "id",
|
|
219
|
-
get: (a) => a.id
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
key: "handle",
|
|
223
|
-
get: (a) => a.handle
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
key: "status",
|
|
227
|
-
get: (a) => a.status
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
key: "pane",
|
|
231
|
-
get: (a) => a.tmux?.pane ?? "-"
|
|
232
|
-
}
|
|
233
|
-
], `${agents.length} sessions`),
|
|
234
|
-
json: agents
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
withGlobals(session.command("focus")).description("move input focus to a peer's session").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
428
|
+
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) => {
|
|
238
429
|
const ctx = ctxOf(opts);
|
|
239
430
|
touch(ctx);
|
|
240
431
|
const target = resolveTarget(ctx, ref);
|
|
@@ -250,11 +441,12 @@ withGlobals(session.command("focus")).description("move input focus to a peer's
|
|
|
250
441
|
}
|
|
251
442
|
});
|
|
252
443
|
});
|
|
253
|
-
withGlobals(
|
|
444
|
+
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) => {
|
|
254
445
|
const ctx = ctxOf(opts);
|
|
255
446
|
touch(ctx);
|
|
256
447
|
const target = resolveTarget(ctx, ref);
|
|
257
|
-
|
|
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);
|
|
258
450
|
emit(formatOf(opts), {
|
|
259
451
|
toon: toonObject({
|
|
260
452
|
nudged: ref,
|
|
@@ -262,11 +454,13 @@ withGlobals(session.command("nudge")).description("ring a peer's session (a dumb
|
|
|
262
454
|
}),
|
|
263
455
|
json: {
|
|
264
456
|
ref,
|
|
265
|
-
pane: target.id
|
|
457
|
+
pane: target.id,
|
|
458
|
+
message,
|
|
459
|
+
resubmits: result.resubmits
|
|
266
460
|
}
|
|
267
461
|
});
|
|
268
462
|
});
|
|
269
|
-
withGlobals(
|
|
463
|
+
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) => {
|
|
270
464
|
const ctx = ctxOf(opts);
|
|
271
465
|
touch(ctx);
|
|
272
466
|
const target = resolveTarget(ctx, ref);
|
|
@@ -278,9 +472,31 @@ withGlobals(session.command("read")).description("scrape a peer's session screen
|
|
|
278
472
|
}, null, 2));
|
|
279
473
|
else console.log(text);
|
|
280
474
|
});
|
|
475
|
+
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
|
+
const ctx = ctxOf(opts);
|
|
477
|
+
touch(ctx);
|
|
478
|
+
let res;
|
|
479
|
+
try {
|
|
480
|
+
res = clearUnit(ctx, ref);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
483
|
+
}
|
|
484
|
+
emit(formatOf(opts), {
|
|
485
|
+
toon: toonObject({
|
|
486
|
+
cleared: ref,
|
|
487
|
+
pane: res.pane,
|
|
488
|
+
command: res.command
|
|
489
|
+
}),
|
|
490
|
+
json: {
|
|
491
|
+
cleared: ref,
|
|
492
|
+
pane: res.pane,
|
|
493
|
+
command: res.command
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
});
|
|
281
497
|
const mail = program.command("mail").description("durable inter-agent messaging");
|
|
282
498
|
function defineSend(cmd) {
|
|
283
|
-
return withGlobals(cmd).description("send a message to a peer (by handle or id)").requiredOption("--to <peer>", "recipient handle or id").option("--from <id>", "sender id (else this session own identity)").option("--subject <s>", "subject").option("--body <text>", "message body").option("--body-file <path>", "read body from a file, or - for stdin").option("--thread <id>", "thread id").option("--reply-to <msg>", "message id this replies to").action((opts) => {
|
|
499
|
+
return withGlobals(cmd).description("send a message to a peer (by handle or id)").requiredOption("--to <peer>", "recipient handle or id").option("--from <id>", "sender id (else this session own identity)").option("--subject <s>", "subject").option("--body <text>", "message body").option("--body-file <path>", "read body from a file, or - for stdin").option("--thread <id>", "thread id").option("--reply-to <msg>", "message id this replies to").option("--no-nudge", "suppress the delivery doorbell (do not wake the recipient's pane)").action(async (opts) => {
|
|
284
500
|
const ctx = ctxOf(opts);
|
|
285
501
|
const fromId = opts.from ?? requireSelf(ctx);
|
|
286
502
|
const body = resolveBody(opts.body, opts.bodyFile);
|
|
@@ -292,21 +508,30 @@ function defineSend(cmd) {
|
|
|
292
508
|
thread: opts.thread,
|
|
293
509
|
replyTo: opts.replyTo
|
|
294
510
|
});
|
|
511
|
+
const wake = await wakeRecipient(ctx.store, () => selectSessionAdapter(ctx.env ?? process.env), realExec, {
|
|
512
|
+
toId: msg.to,
|
|
513
|
+
fromId,
|
|
514
|
+
noNudge: opts.nudge === false
|
|
515
|
+
});
|
|
516
|
+
if (wake.warning) console.error(`delivery doorbell not confirmed (message still delivered): ${wake.warning}`);
|
|
295
517
|
emit(formatOf(opts), {
|
|
296
518
|
toon: toonObject({
|
|
297
519
|
sent: msg.id,
|
|
298
520
|
to: opts.to,
|
|
299
|
-
subject: msg.subject
|
|
521
|
+
subject: msg.subject,
|
|
522
|
+
rung: wake.rung
|
|
300
523
|
}),
|
|
301
|
-
json:
|
|
524
|
+
json: {
|
|
525
|
+
...msg,
|
|
526
|
+
rung: wake.rung
|
|
527
|
+
}
|
|
302
528
|
});
|
|
303
529
|
});
|
|
304
530
|
}
|
|
305
531
|
defineSend(mail.command("send"));
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const meId = requireSelf(ctx);
|
|
532
|
+
/** List a resolved inbox (the caller's own or an --owner mailbox) as TOON with an aggregate;
|
|
533
|
+
* `readCmd` is the follow-up read command prefix surfaced as the next step. */
|
|
534
|
+
function emitInbox(ctx, opts, meId, readCmd) {
|
|
310
535
|
const items = inbox({ store: ctx.store }, {
|
|
311
536
|
meId,
|
|
312
537
|
unread: opts.unread,
|
|
@@ -336,12 +561,34 @@ function runInbox(opts) {
|
|
|
336
561
|
json: items
|
|
337
562
|
});
|
|
338
563
|
const firstUnread = items.find((m) => !m.read);
|
|
339
|
-
if (firstUnread) nextStep(
|
|
564
|
+
if (firstUnread) nextStep(`${readCmd} ${firstUnread.id}`);
|
|
340
565
|
}
|
|
341
|
-
|
|
342
|
-
withGlobals(mail.command("read")).description("peek at a message without acknowledging it").argument("<msg-id>", "message id").action((msgId, opts) => {
|
|
566
|
+
function runInbox(opts) {
|
|
343
567
|
const ctx = ctxOf(opts);
|
|
344
|
-
|
|
568
|
+
touch(ctx);
|
|
569
|
+
emitInbox(ctx, opts, opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx), "cyberlegion mail read");
|
|
570
|
+
}
|
|
571
|
+
withGlobals(mail.command("inbox")).description("list your mail").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").option("--owner <handle>", "target a standing owner's mailbox instead of this session's own").action(runInbox);
|
|
572
|
+
withGlobals(mail.command("read")).description("read a message; --ack consumes it in the same step, otherwise it only peeks").argument("<msg-id>", "message id").option("--ack", "acknowledge the message in the same step (idempotent — no error if already acked)").option("--owner <handle>", "read a standing owner's mailbox instead of this session's own").action((msgId, opts) => {
|
|
573
|
+
const ctx = ctxOf(opts);
|
|
574
|
+
const meId = opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx);
|
|
575
|
+
if (opts.ack) {
|
|
576
|
+
const { msg, acked } = readAck({ store: ctx.store }, meId, msgId);
|
|
577
|
+
emit(formatOf(opts), {
|
|
578
|
+
toon: toonObject({
|
|
579
|
+
id: msg.id,
|
|
580
|
+
from: msg.fromHandle,
|
|
581
|
+
subject: msg.subject,
|
|
582
|
+
body: msg.body,
|
|
583
|
+
acked
|
|
584
|
+
}),
|
|
585
|
+
json: {
|
|
586
|
+
...msg,
|
|
587
|
+
acked
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
345
592
|
const msg = peek({ store: ctx.store }, meId, msgId);
|
|
346
593
|
if (!msg) fail(`"${msgId}" is not a message in this inbox`);
|
|
347
594
|
emit(formatOf(opts), {
|
|
@@ -353,11 +600,11 @@ withGlobals(mail.command("read")).description("peek at a message without acknowl
|
|
|
353
600
|
}),
|
|
354
601
|
json: msg
|
|
355
602
|
});
|
|
356
|
-
nextStep(`cyberlegion mail ack ${msg.id}`);
|
|
603
|
+
nextStep(`cyberlegion mail ack ${msg.id}${opts.owner ? ` --owner ${opts.owner}` : ""}`);
|
|
357
604
|
});
|
|
358
|
-
withGlobals(mail.command("ack")).description("acknowledge a message (moves it out of the unread set)").argument("<msg-id>", "message id").action((msgId, opts) => {
|
|
605
|
+
withGlobals(mail.command("ack")).description("acknowledge a message (moves it out of the unread set)").argument("<msg-id>", "message id").option("--owner <handle>", "ack a standing owner's mailbox instead of this session's own").action((msgId, opts) => {
|
|
359
606
|
const ctx = ctxOf(opts);
|
|
360
|
-
const meId = requireSelf(ctx);
|
|
607
|
+
const meId = opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx);
|
|
361
608
|
const msg = ack({ store: ctx.store }, meId, msgId);
|
|
362
609
|
emit(formatOf(opts), {
|
|
363
610
|
toon: toonObject({
|
|
@@ -431,95 +678,6 @@ withGlobals(mail.command("hook")).description("emit the harness hook injection p
|
|
|
431
678
|
const payload = injectInbox(ctx, opts.event);
|
|
432
679
|
if (payload) console.log(JSON.stringify(payload));
|
|
433
680
|
});
|
|
434
|
-
const dispatch = program.command("dispatch").description("delegate work and await a result (result-slot primitives)");
|
|
435
|
-
function definePrepOptions(cmd) {
|
|
436
|
-
return withGlobals(cmd).option("--agent <name>", "resolve an agent def to build the instruction/launch from").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--role <name>", "role label folded into the generic instruction when no agent def is given").option("--brief-text <text>", "brief body text").option("--brief-file <path>", "read the brief from a file, or - for stdin").option("--verdict-schema <path>", "JSON schema file (required keys + primitive types) the result must satisfy").option("--thread <id>", "thread id (defaults to the minted dispatch id)");
|
|
437
|
-
}
|
|
438
|
-
function dispatchResultFields(r) {
|
|
439
|
-
return {
|
|
440
|
-
id: r.id,
|
|
441
|
-
verdict: r.verdict != null ? JSON.stringify(r.verdict) : void 0,
|
|
442
|
-
body: r.body,
|
|
443
|
-
ts: r.ts
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
definePrepOptions(dispatch.command("prep")).description("allocate an id + brief + result slot and return the envelope — spawns nothing, never invokes Task").action((opts) => {
|
|
447
|
-
const ctx = ctxOf(opts);
|
|
448
|
-
let envelope;
|
|
449
|
-
try {
|
|
450
|
-
envelope = prep({ store: ctx.store }, {
|
|
451
|
-
agent: opts.agent,
|
|
452
|
-
agentFile: opts.agentFile,
|
|
453
|
-
role: opts.role,
|
|
454
|
-
briefText: opts.briefText,
|
|
455
|
-
briefFile: opts.briefFile,
|
|
456
|
-
thread: opts.thread
|
|
457
|
-
});
|
|
458
|
-
} catch (err) {
|
|
459
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
460
|
-
}
|
|
461
|
-
emit(formatOf(opts), {
|
|
462
|
-
toon: toonObject({ ...envelope }),
|
|
463
|
-
json: envelope
|
|
464
|
-
});
|
|
465
|
-
nextStep(`spawn a subagent with the instruction, then \`cyberlegion dispatch collect ${envelope.id}\` — or on the channel path, \`cyberlegion mail await --thread ${envelope.thread}\``);
|
|
466
|
-
});
|
|
467
|
-
definePrepOptions(dispatch.command("channel")).description("prep + spawn a peer session + (--wait) await the mail-thread reply — the one CLI-driven convenience (needs --agent or --agent-file to realize the peer launch)").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
468
|
-
"pane:right",
|
|
469
|
-
"pane:down",
|
|
470
|
-
"tab",
|
|
471
|
-
"window"
|
|
472
|
-
]).default("pane:right")).option("--wait", "block for the mail-thread reply and print the validated result").option("--timeout <ms>", "give up after this many ms with no reply (0 = wait forever)", (v) => Number.parseInt(v, 10), 6e5).option("--max-wait <s>", "self-cap for one internal poll cycle, in seconds (re-arm by re-running with --wait)", (v) => Number.parseInt(v, 10), 240).action(async (opts) => {
|
|
473
|
-
const ctx = ctxOf(opts);
|
|
474
|
-
touch(ctx);
|
|
475
|
-
let result;
|
|
476
|
-
try {
|
|
477
|
-
result = await channel(ctx, {
|
|
478
|
-
agent: opts.agent,
|
|
479
|
-
agentFile: opts.agentFile,
|
|
480
|
-
role: opts.role,
|
|
481
|
-
briefText: opts.briefText,
|
|
482
|
-
briefFile: opts.briefFile,
|
|
483
|
-
verdictSchema: opts.verdictSchema,
|
|
484
|
-
thread: opts.thread,
|
|
485
|
-
at: opts.at,
|
|
486
|
-
wait: opts.wait,
|
|
487
|
-
timeoutMs: opts.timeout,
|
|
488
|
-
maxWaitS: opts.maxWait
|
|
489
|
-
});
|
|
490
|
-
} catch (err) {
|
|
491
|
-
if (err instanceof DispatchWaitingError) {
|
|
492
|
-
console.error(err.message);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
496
|
-
}
|
|
497
|
-
if ("ts" in result) {
|
|
498
|
-
emit(formatOf(opts), {
|
|
499
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
500
|
-
json: result
|
|
501
|
-
});
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
emit(formatOf(opts), {
|
|
505
|
-
toon: toonObject({ ...result }),
|
|
506
|
-
json: result
|
|
507
|
-
});
|
|
508
|
-
nextStep(`cyberlegion mail await --thread ${result.thread} to collect the reply later`);
|
|
509
|
-
});
|
|
510
|
-
withGlobals(dispatch.command("collect")).description("read + validate a subagent's result file (the subagent path's counterpart to mail await)").argument("<id>", "dispatch id").option("--verdict-schema <path>", "JSON schema file the result must satisfy").action((id, opts) => {
|
|
511
|
-
const ctx = ctxOf(opts);
|
|
512
|
-
let result;
|
|
513
|
-
try {
|
|
514
|
-
result = collect({ store: ctx.store }, id, opts.verdictSchema);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
517
|
-
}
|
|
518
|
-
emit(formatOf(opts), {
|
|
519
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
520
|
-
json: result
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
681
|
const agent = program.command("agent").description("resolve reusable agent definitions");
|
|
524
682
|
const INSTRUCTIONS_PREVIEW_LEN = 200;
|
|
525
683
|
function truncated(text, full) {
|
|
@@ -611,31 +769,11 @@ withGlobals(agent.command("path")).description("print the resolved def file path
|
|
|
611
769
|
json: { path: def.path }
|
|
612
770
|
});
|
|
613
771
|
});
|
|
614
|
-
const
|
|
615
|
-
withGlobals(
|
|
616
|
-
const results = install(opts.agent, opts.dir);
|
|
617
|
-
emit(formatOf(opts), {
|
|
618
|
-
toon: toonList("hooks", results, [
|
|
619
|
-
{
|
|
620
|
-
key: "event",
|
|
621
|
-
get: (r) => r.event
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
key: "status",
|
|
625
|
-
get: (r) => r.status
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
key: "file",
|
|
629
|
-
get: (r) => r.file
|
|
630
|
-
}
|
|
631
|
-
], `${results.length} hooks`),
|
|
632
|
-
json: results
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
withGlobals(admin.command("doctor")).description("probe harness, multiplexer (ancestry-discovered), hub root, and self-id").action((opts) => {
|
|
772
|
+
const mux = program.command("mux").description("the unit-agnostic pane layer — multiplexer detection and diagnostics");
|
|
773
|
+
withGlobals(mux.command("doctor")).description("probe harness, multiplexer (ancestry-discovered), hub root, and self-id").action((opts) => {
|
|
636
774
|
const ctx = ctxOf(opts);
|
|
637
775
|
const harness = detectHarness(void 0, ctx) ?? "unknown";
|
|
638
|
-
const probe = probeMultiplexer(ctx.exec ?? realExec, ctx.env ?? process.env);
|
|
776
|
+
const probe = probeMultiplexer(ctx.exec ?? realExec, normalizeMuxEnv(ctx.env ?? process.env));
|
|
639
777
|
const selfId = resolveSelfId(ctx) ?? "-";
|
|
640
778
|
emit(formatOf(opts), {
|
|
641
779
|
toon: toonObject({
|
|
@@ -655,19 +793,46 @@ withGlobals(admin.command("doctor")).description("probe harness, multiplexer (an
|
|
|
655
793
|
selfId
|
|
656
794
|
}
|
|
657
795
|
});
|
|
658
|
-
if (probe.mux !== "none") nextStep(`export
|
|
796
|
+
if (probe.mux !== "none") nextStep(`export CYBER_MUX=${probe.mux}${probe.pane ? ` CYBER_MUX_PANE=${probe.pane}` : ""} — pin the fast-path, skip ancestry discovery on later calls`);
|
|
659
797
|
});
|
|
660
|
-
withGlobals(
|
|
661
|
-
|
|
798
|
+
withGlobals(mux.command("mode")).description("report the detected session-backend mode").action((opts) => {
|
|
799
|
+
const ctx = ctxOf(opts);
|
|
800
|
+
let mode = "none";
|
|
662
801
|
try {
|
|
663
|
-
|
|
802
|
+
mode = selectSessionAdapter(ctx.env ?? process.env).name;
|
|
664
803
|
} catch {}
|
|
665
804
|
emit(formatOf(opts), {
|
|
666
|
-
toon: toonObject({ mode
|
|
667
|
-
json: { mode
|
|
805
|
+
toon: toonObject({ mode }),
|
|
806
|
+
json: { mode }
|
|
668
807
|
});
|
|
669
808
|
});
|
|
670
|
-
withGlobals(
|
|
809
|
+
withGlobals(program.command("attach")).description("bind this pane as the hub's main pane (the owner's live presence); --show reads it, --clear unbinds").option("--clear", "unbind the main pane (a no-op when nothing is bound)").option("--show", "print the bound main pane instead of binding").action((opts) => {
|
|
810
|
+
const ctx = ctxOf(opts);
|
|
811
|
+
if (opts.show) {
|
|
812
|
+
const pane = ctx.store.getMainPane();
|
|
813
|
+
emit(formatOf(opts), {
|
|
814
|
+
toon: toonObject({ mainPane: pane ?? "none" }),
|
|
815
|
+
json: { mainPane: pane ?? null }
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (opts.clear) {
|
|
820
|
+
ctx.store.setMainPane(null);
|
|
821
|
+
emit(formatOf(opts), {
|
|
822
|
+
toon: toonObject({ mainPane: "none" }),
|
|
823
|
+
json: { mainPane: null }
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const cur = currentPane(normalizeMuxEnv(ctx.env ?? process.env));
|
|
828
|
+
if (!cur) fail("no multiplexer pane to bind — run this from inside a tmux or herdr pane");
|
|
829
|
+
ctx.store.setMainPane(cur.pane);
|
|
830
|
+
emit(formatOf(opts), {
|
|
831
|
+
toon: toonObject({ mainPane: cur.pane }),
|
|
832
|
+
json: { mainPane: cur.pane }
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
withGlobals(program.command("admin").description("hub-state maintenance").command("migrate")).description("merge one hub root state into another (e.g. an old project-local root into the global hub)").requiredOption("--from <path>", "source hub root").requiredOption("--to <path>", "destination hub root").action((opts) => {
|
|
671
836
|
const res = migrateStore(new FileStore(resolve(opts.from)), new FileStore(resolve(opts.to)));
|
|
672
837
|
emit(formatOf(opts), {
|
|
673
838
|
toon: toonObject({
|
|
@@ -678,10 +843,51 @@ withGlobals(admin.command("migrate")).description("merge one hub root state into
|
|
|
678
843
|
json: res
|
|
679
844
|
});
|
|
680
845
|
});
|
|
846
|
+
withGlobals(program.command("init")).description("resolve this session harness, register the Legion surfacing hook, and advise owner binding").option("--agent <h>", "claude | cursor | codex (else auto-detected)").option("--dir <path>", "project dir to write config into", process.cwd()).option("--pin <version>", "version to pin the registered npx hook command to (e.g. the bundled plugin version)").action((opts) => {
|
|
847
|
+
const ctx = ctxOf(opts);
|
|
848
|
+
let harness;
|
|
849
|
+
try {
|
|
850
|
+
const detected = detectHarness(opts.agent, ctx);
|
|
851
|
+
if (!detected) fail("could not detect harness — pass --agent claude|cursor|codex");
|
|
852
|
+
harness = detected;
|
|
853
|
+
} catch (err) {
|
|
854
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
855
|
+
}
|
|
856
|
+
let results;
|
|
857
|
+
try {
|
|
858
|
+
results = install(harness, opts.dir, opts.pin);
|
|
859
|
+
} catch (err) {
|
|
860
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
861
|
+
}
|
|
862
|
+
emit(formatOf(opts), {
|
|
863
|
+
toon: toonList("hooks", results, [
|
|
864
|
+
{
|
|
865
|
+
key: "event",
|
|
866
|
+
get: (r) => r.event
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
key: "status",
|
|
870
|
+
get: (r) => r.status
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
key: "file",
|
|
874
|
+
get: (r) => r.file
|
|
875
|
+
}
|
|
876
|
+
], `harness ${harness}, ${results.length} hooks`),
|
|
877
|
+
json: {
|
|
878
|
+
harness,
|
|
879
|
+
hooks: results
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
if (!listAgents(ctx.store).some((a) => a.kind === "standing")) {
|
|
883
|
+
nextStep("cyberlegion unit register --standing --handle <name> to mint the durable owner inbox");
|
|
884
|
+
nextStep("cyberlegion attach to bind this pane as the owner live presence");
|
|
885
|
+
}
|
|
886
|
+
});
|
|
681
887
|
defineSpawn(program.command("spawn"));
|
|
682
888
|
defineSend(program.command("send"));
|
|
683
889
|
withGlobals(program.command("inbox")).description("list your mail (alias of `mail inbox`)").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").action(runInbox);
|
|
684
|
-
withGlobals(program.command("who")).description("list the addressable
|
|
890
|
+
withGlobals(program.command("who")).description("list the addressable units (alias of `unit who`)").option("--all", "include exited units").option("--reconcile", "live-probe the current mux: cull dead-pane records and adopt unbound harness-bearing panes before listing").action(runWho);
|
|
685
891
|
withGlobals(program).action((opts) => {
|
|
686
892
|
const ctx = ctxOf(opts);
|
|
687
893
|
touch(ctx);
|
|
@@ -709,7 +915,7 @@ withGlobals(program).action((opts) => {
|
|
|
709
915
|
units
|
|
710
916
|
}
|
|
711
917
|
});
|
|
712
|
-
if (!id) nextStep("cyberlegion
|
|
918
|
+
if (!id) nextStep("cyberlegion unit register to join");
|
|
713
919
|
else if (unread > 0) nextStep("cyberlegion mail inbox --unread");
|
|
714
920
|
});
|
|
715
921
|
program.parseAsync(process.argv).catch((err) => {
|