cyberlegion 0.1.0 → 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 +379 -216
- package/dist/index.d.mts +57 -279
- package/dist/index.mjs +4 -28
- package/dist/{watch-BuYsdwhx.mjs → watch-nkm6F1_f.mjs} +696 -745
- package/package.json +7 -5
- package/readme.md +35 -30
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
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,41 +178,10 @@ 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
|
-
|
|
71
|
-
handle: opts.handle,
|
|
72
|
-
harness: opts.harness
|
|
73
|
-
});
|
|
74
|
-
emit(formatOf(opts), {
|
|
75
|
-
toon: toonObject({
|
|
76
|
-
id: rec.id,
|
|
77
|
-
handle: rec.handle,
|
|
78
|
-
harness: rec.harness ?? "-",
|
|
79
|
-
status: rec.status
|
|
80
|
-
}),
|
|
81
|
-
json: rec
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
withGlobals(identity.command("whoami")).description("print this session own identity").action((opts) => {
|
|
85
|
-
const ctx = ctxOf(opts);
|
|
86
|
-
const id = resolveSelfId(ctx);
|
|
87
|
-
if (!id) fail("no identity in this session — run `cyberlegion identity register` first");
|
|
88
|
-
const rec = loadAgent(ctx.store, id);
|
|
89
|
-
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
90
|
-
emit(formatOf(opts), {
|
|
91
|
-
toon: toonObject({
|
|
92
|
-
id: rec.id,
|
|
93
|
-
handle: rec.handle,
|
|
94
|
-
harness: rec.harness ?? "-",
|
|
95
|
-
status: rec.status
|
|
96
|
-
}),
|
|
97
|
-
json: rec
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
withGlobals(identity.command("owner")).description("mint (or refresh) a standing, session-independent owner inbox").option("--handle <name>", "the owner handle to claim").action((opts) => {
|
|
101
|
-
const ctx = ctxOf(opts);
|
|
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) {
|
|
102
185
|
if (!opts.handle) {
|
|
103
186
|
const standing = listAgents(ctx.store).filter((a) => a.kind === "standing");
|
|
104
187
|
emit(formatOf(opts), {
|
|
@@ -119,7 +202,7 @@ withGlobals(identity.command("owner")).description("mint (or refresh) a standing
|
|
|
119
202
|
key: "status",
|
|
120
203
|
get: (a) => a.status
|
|
121
204
|
}
|
|
122
|
-
], `${standing.length}
|
|
205
|
+
], `${standing.length} standing`),
|
|
123
206
|
json: standing
|
|
124
207
|
});
|
|
125
208
|
return;
|
|
@@ -136,13 +219,94 @@ withGlobals(identity.command("owner")).description("mint (or refresh) a standing
|
|
|
136
219
|
}),
|
|
137
220
|
json: rec
|
|
138
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, {
|
|
230
|
+
handle: opts.handle,
|
|
231
|
+
harness: opts.harness
|
|
232
|
+
});
|
|
233
|
+
emit(formatOf(opts), {
|
|
234
|
+
toon: toonObject({
|
|
235
|
+
id: rec.id,
|
|
236
|
+
handle: rec.handle,
|
|
237
|
+
harness: rec.harness ?? "-",
|
|
238
|
+
status: rec.status
|
|
239
|
+
}),
|
|
240
|
+
json: rec
|
|
241
|
+
});
|
|
242
|
+
});
|
|
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) => {
|
|
288
|
+
const ctx = ctxOf(opts);
|
|
289
|
+
const id = resolveSelfId(ctx);
|
|
290
|
+
if (!id) fail("no identity in this session — run `cyberlegion unit register` first");
|
|
291
|
+
const rec = loadAgent(ctx.store, id);
|
|
292
|
+
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
293
|
+
emit(formatOf(opts), {
|
|
294
|
+
toon: toonObject({
|
|
295
|
+
id: rec.id,
|
|
296
|
+
handle: rec.handle,
|
|
297
|
+
harness: rec.harness ?? "-",
|
|
298
|
+
status: rec.status
|
|
299
|
+
}),
|
|
300
|
+
json: rec
|
|
301
|
+
});
|
|
139
302
|
});
|
|
140
303
|
function runWho(opts) {
|
|
141
304
|
const ctx = ctxOf(opts);
|
|
142
305
|
touch(ctx);
|
|
306
|
+
if (opts.reconcile) reconcile(ctx, { adopt: true });
|
|
143
307
|
const agents = listAgents(ctx.store).filter((a) => opts.all || a.status !== "exited");
|
|
144
308
|
emit(formatOf(opts), {
|
|
145
|
-
toon: toonList("
|
|
309
|
+
toon: toonList("units", agents, [
|
|
146
310
|
{
|
|
147
311
|
key: "id",
|
|
148
312
|
get: (a) => a.id
|
|
@@ -158,14 +322,18 @@ function runWho(opts) {
|
|
|
158
322
|
{
|
|
159
323
|
key: "status",
|
|
160
324
|
get: (a) => a.status
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
key: "pane",
|
|
328
|
+
get: (a) => a.pane?.id ?? "-"
|
|
161
329
|
}
|
|
162
|
-
], `${agents.length}
|
|
330
|
+
], `${agents.length} units`),
|
|
163
331
|
json: agents
|
|
164
332
|
});
|
|
165
|
-
if (agents.length === 0) nextStep("cyberlegion
|
|
333
|
+
if (agents.length === 0) nextStep("cyberlegion unit register to join");
|
|
166
334
|
}
|
|
167
|
-
withGlobals(
|
|
168
|
-
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) => {
|
|
169
337
|
const ctx = ctxOf(opts);
|
|
170
338
|
touch(ctx);
|
|
171
339
|
const changed = prune(ctx);
|
|
@@ -180,15 +348,13 @@ withGlobals(identity.command("prune")).description("mark dead agents exited and
|
|
|
180
348
|
json: changed
|
|
181
349
|
});
|
|
182
350
|
});
|
|
183
|
-
const session = program.command("session").description("warm peer session lifecycle over a multiplexer");
|
|
184
351
|
function defineSpawn(cmd) {
|
|
185
|
-
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").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([
|
|
186
353
|
"pane:right",
|
|
187
354
|
"pane:down",
|
|
188
355
|
"tab",
|
|
189
|
-
"window",
|
|
190
356
|
"workspace"
|
|
191
|
-
]).
|
|
357
|
+
])).option("--no-wake", "suppress the first-turn doorbell (spawn idle; the caller drives the first turn itself)").action(async (opts) => {
|
|
192
358
|
const ctx = ctxOf(opts);
|
|
193
359
|
touch(ctx);
|
|
194
360
|
let harness = opts.harness;
|
|
@@ -207,7 +373,7 @@ function defineSpawn(cmd) {
|
|
|
207
373
|
harness = realized.harness;
|
|
208
374
|
command = realized.command;
|
|
209
375
|
}
|
|
210
|
-
if (!harness) fail("
|
|
376
|
+
if (!harness) fail("unit spawn needs --harness, or --agent/--agent-file resolving one");
|
|
211
377
|
const res = spawn(ctx, {
|
|
212
378
|
harness,
|
|
213
379
|
command,
|
|
@@ -219,21 +385,30 @@ function defineSpawn(cmd) {
|
|
|
219
385
|
cwd: opts.cwd,
|
|
220
386
|
at: opts.at
|
|
221
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}`);
|
|
222
393
|
emit(formatOf(opts), {
|
|
223
394
|
toon: toonObject({
|
|
224
395
|
spawned: res.agent.id,
|
|
225
396
|
handle: res.agent.handle,
|
|
226
397
|
harness: res.agent.harness,
|
|
227
398
|
worktree: res.agent.worktree?.root,
|
|
228
|
-
pane: res.pane
|
|
399
|
+
pane: res.pane,
|
|
400
|
+
rung: wake.rung
|
|
229
401
|
}),
|
|
230
|
-
json:
|
|
402
|
+
json: {
|
|
403
|
+
...res,
|
|
404
|
+
rung: wake.rung
|
|
405
|
+
}
|
|
231
406
|
});
|
|
232
|
-
nextStep(`cyberlegion
|
|
407
|
+
nextStep(`cyberlegion unit read ${res.agent.id}`);
|
|
233
408
|
});
|
|
234
409
|
}
|
|
235
|
-
defineSpawn(
|
|
236
|
-
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) => {
|
|
237
412
|
const ctx = ctxOf(opts);
|
|
238
413
|
touch(ctx);
|
|
239
414
|
const agent = resolveAgent(ctx.store, ref);
|
|
@@ -250,33 +425,7 @@ withGlobals(session.command("close")).description("tear down a unit's worktree +
|
|
|
250
425
|
json: res
|
|
251
426
|
});
|
|
252
427
|
});
|
|
253
|
-
withGlobals(
|
|
254
|
-
const ctx = ctxOf(opts);
|
|
255
|
-
touch(ctx);
|
|
256
|
-
const agents = listAgents(ctx.store).filter((a) => a.status !== "exited");
|
|
257
|
-
emit(formatOf(opts), {
|
|
258
|
-
toon: toonList("sessions", agents, [
|
|
259
|
-
{
|
|
260
|
-
key: "id",
|
|
261
|
-
get: (a) => a.id
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
key: "handle",
|
|
265
|
-
get: (a) => a.handle
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
key: "status",
|
|
269
|
-
get: (a) => a.status
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
key: "pane",
|
|
273
|
-
get: (a) => a.tmux?.pane ?? "-"
|
|
274
|
-
}
|
|
275
|
-
], `${agents.length} sessions`),
|
|
276
|
-
json: agents
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
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) => {
|
|
280
429
|
const ctx = ctxOf(opts);
|
|
281
430
|
touch(ctx);
|
|
282
431
|
const target = resolveTarget(ctx, ref);
|
|
@@ -292,11 +441,12 @@ withGlobals(session.command("focus")).description("move input focus to a peer's
|
|
|
292
441
|
}
|
|
293
442
|
});
|
|
294
443
|
});
|
|
295
|
-
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) => {
|
|
296
445
|
const ctx = ctxOf(opts);
|
|
297
446
|
touch(ctx);
|
|
298
447
|
const target = resolveTarget(ctx, ref);
|
|
299
|
-
|
|
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);
|
|
300
450
|
emit(formatOf(opts), {
|
|
301
451
|
toon: toonObject({
|
|
302
452
|
nudged: ref,
|
|
@@ -304,11 +454,13 @@ withGlobals(session.command("nudge")).description("ring a peer's session (a dumb
|
|
|
304
454
|
}),
|
|
305
455
|
json: {
|
|
306
456
|
ref,
|
|
307
|
-
pane: target.id
|
|
457
|
+
pane: target.id,
|
|
458
|
+
message,
|
|
459
|
+
resubmits: result.resubmits
|
|
308
460
|
}
|
|
309
461
|
});
|
|
310
462
|
});
|
|
311
|
-
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) => {
|
|
312
464
|
const ctx = ctxOf(opts);
|
|
313
465
|
touch(ctx);
|
|
314
466
|
const target = resolveTarget(ctx, ref);
|
|
@@ -320,9 +472,31 @@ withGlobals(session.command("read")).description("scrape a peer's session screen
|
|
|
320
472
|
}, null, 2));
|
|
321
473
|
else console.log(text);
|
|
322
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
|
+
});
|
|
323
497
|
const mail = program.command("mail").description("durable inter-agent messaging");
|
|
324
498
|
function defineSend(cmd) {
|
|
325
|
-
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) => {
|
|
326
500
|
const ctx = ctxOf(opts);
|
|
327
501
|
const fromId = opts.from ?? requireSelf(ctx);
|
|
328
502
|
const body = resolveBody(opts.body, opts.bodyFile);
|
|
@@ -334,21 +508,30 @@ function defineSend(cmd) {
|
|
|
334
508
|
thread: opts.thread,
|
|
335
509
|
replyTo: opts.replyTo
|
|
336
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}`);
|
|
337
517
|
emit(formatOf(opts), {
|
|
338
518
|
toon: toonObject({
|
|
339
519
|
sent: msg.id,
|
|
340
520
|
to: opts.to,
|
|
341
|
-
subject: msg.subject
|
|
521
|
+
subject: msg.subject,
|
|
522
|
+
rung: wake.rung
|
|
342
523
|
}),
|
|
343
|
-
json:
|
|
524
|
+
json: {
|
|
525
|
+
...msg,
|
|
526
|
+
rung: wake.rung
|
|
527
|
+
}
|
|
344
528
|
});
|
|
345
529
|
});
|
|
346
530
|
}
|
|
347
531
|
defineSend(mail.command("send"));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const meId = opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : 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) {
|
|
352
535
|
const items = inbox({ store: ctx.store }, {
|
|
353
536
|
meId,
|
|
354
537
|
unread: opts.unread,
|
|
@@ -378,12 +561,34 @@ function runInbox(opts) {
|
|
|
378
561
|
json: items
|
|
379
562
|
});
|
|
380
563
|
const firstUnread = items.find((m) => !m.read);
|
|
381
|
-
if (firstUnread) nextStep(
|
|
564
|
+
if (firstUnread) nextStep(`${readCmd} ${firstUnread.id}`);
|
|
565
|
+
}
|
|
566
|
+
function runInbox(opts) {
|
|
567
|
+
const ctx = ctxOf(opts);
|
|
568
|
+
touch(ctx);
|
|
569
|
+
emitInbox(ctx, opts, opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx), "cyberlegion mail read");
|
|
382
570
|
}
|
|
383
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);
|
|
384
|
-
withGlobals(mail.command("read")).description("
|
|
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) => {
|
|
385
573
|
const ctx = ctxOf(opts);
|
|
386
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
|
+
}
|
|
387
592
|
const msg = peek({ store: ctx.store }, meId, msgId);
|
|
388
593
|
if (!msg) fail(`"${msgId}" is not a message in this inbox`);
|
|
389
594
|
emit(formatOf(opts), {
|
|
@@ -473,96 +678,6 @@ withGlobals(mail.command("hook")).description("emit the harness hook injection p
|
|
|
473
678
|
const payload = injectInbox(ctx, opts.event);
|
|
474
679
|
if (payload) console.log(JSON.stringify(payload));
|
|
475
680
|
});
|
|
476
|
-
const dispatch = program.command("dispatch").description("delegate work and await a result (result-slot primitives)");
|
|
477
|
-
function definePrepOptions(cmd) {
|
|
478
|
-
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)");
|
|
479
|
-
}
|
|
480
|
-
function dispatchResultFields(r) {
|
|
481
|
-
return {
|
|
482
|
-
id: r.id,
|
|
483
|
-
verdict: r.verdict != null ? JSON.stringify(r.verdict) : void 0,
|
|
484
|
-
body: r.body,
|
|
485
|
-
ts: r.ts
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
definePrepOptions(dispatch.command("prep")).description("allocate an id + brief + result slot and return the envelope — spawns nothing, never invokes Task").action((opts) => {
|
|
489
|
-
const ctx = ctxOf(opts);
|
|
490
|
-
let envelope;
|
|
491
|
-
try {
|
|
492
|
-
envelope = prep({ store: ctx.store }, {
|
|
493
|
-
agent: opts.agent,
|
|
494
|
-
agentFile: opts.agentFile,
|
|
495
|
-
role: opts.role,
|
|
496
|
-
briefText: opts.briefText,
|
|
497
|
-
briefFile: opts.briefFile,
|
|
498
|
-
thread: opts.thread
|
|
499
|
-
});
|
|
500
|
-
} catch (err) {
|
|
501
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
502
|
-
}
|
|
503
|
-
emit(formatOf(opts), {
|
|
504
|
-
toon: toonObject({ ...envelope }),
|
|
505
|
-
json: envelope
|
|
506
|
-
});
|
|
507
|
-
nextStep(`spawn a subagent with the instruction, then \`cyberlegion dispatch collect ${envelope.id}\` — or on the channel path, \`cyberlegion mail await --thread ${envelope.thread}\``);
|
|
508
|
-
});
|
|
509
|
-
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([
|
|
510
|
-
"pane:right",
|
|
511
|
-
"pane:down",
|
|
512
|
-
"tab",
|
|
513
|
-
"window",
|
|
514
|
-
"workspace"
|
|
515
|
-
]).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) => {
|
|
516
|
-
const ctx = ctxOf(opts);
|
|
517
|
-
touch(ctx);
|
|
518
|
-
let result;
|
|
519
|
-
try {
|
|
520
|
-
result = await channel(ctx, {
|
|
521
|
-
agent: opts.agent,
|
|
522
|
-
agentFile: opts.agentFile,
|
|
523
|
-
role: opts.role,
|
|
524
|
-
briefText: opts.briefText,
|
|
525
|
-
briefFile: opts.briefFile,
|
|
526
|
-
verdictSchema: opts.verdictSchema,
|
|
527
|
-
thread: opts.thread,
|
|
528
|
-
at: opts.at,
|
|
529
|
-
wait: opts.wait,
|
|
530
|
-
timeoutMs: opts.timeout,
|
|
531
|
-
maxWaitS: opts.maxWait
|
|
532
|
-
});
|
|
533
|
-
} catch (err) {
|
|
534
|
-
if (err instanceof DispatchWaitingError) {
|
|
535
|
-
console.error(err.message);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
539
|
-
}
|
|
540
|
-
if ("ts" in result) {
|
|
541
|
-
emit(formatOf(opts), {
|
|
542
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
543
|
-
json: result
|
|
544
|
-
});
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
emit(formatOf(opts), {
|
|
548
|
-
toon: toonObject({ ...result }),
|
|
549
|
-
json: result
|
|
550
|
-
});
|
|
551
|
-
nextStep(`cyberlegion mail await --thread ${result.thread} to collect the reply later`);
|
|
552
|
-
});
|
|
553
|
-
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) => {
|
|
554
|
-
const ctx = ctxOf(opts);
|
|
555
|
-
let result;
|
|
556
|
-
try {
|
|
557
|
-
result = collect({ store: ctx.store }, id, opts.verdictSchema);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
560
|
-
}
|
|
561
|
-
emit(formatOf(opts), {
|
|
562
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
563
|
-
json: result
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
681
|
const agent = program.command("agent").description("resolve reusable agent definitions");
|
|
567
682
|
const INSTRUCTIONS_PREVIEW_LEN = 200;
|
|
568
683
|
function truncated(text, full) {
|
|
@@ -654,31 +769,11 @@ withGlobals(agent.command("path")).description("print the resolved def file path
|
|
|
654
769
|
json: { path: def.path }
|
|
655
770
|
});
|
|
656
771
|
});
|
|
657
|
-
const
|
|
658
|
-
withGlobals(
|
|
659
|
-
const results = install(opts.agent, opts.dir);
|
|
660
|
-
emit(formatOf(opts), {
|
|
661
|
-
toon: toonList("hooks", results, [
|
|
662
|
-
{
|
|
663
|
-
key: "event",
|
|
664
|
-
get: (r) => r.event
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
key: "status",
|
|
668
|
-
get: (r) => r.status
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
key: "file",
|
|
672
|
-
get: (r) => r.file
|
|
673
|
-
}
|
|
674
|
-
], `${results.length} hooks`),
|
|
675
|
-
json: results
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
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) => {
|
|
679
774
|
const ctx = ctxOf(opts);
|
|
680
775
|
const harness = detectHarness(void 0, ctx) ?? "unknown";
|
|
681
|
-
const probe = probeMultiplexer(ctx.exec ?? realExec, ctx.env ?? process.env);
|
|
776
|
+
const probe = probeMultiplexer(ctx.exec ?? realExec, normalizeMuxEnv(ctx.env ?? process.env));
|
|
682
777
|
const selfId = resolveSelfId(ctx) ?? "-";
|
|
683
778
|
emit(formatOf(opts), {
|
|
684
779
|
toon: toonObject({
|
|
@@ -698,19 +793,46 @@ withGlobals(admin.command("doctor")).description("probe harness, multiplexer (an
|
|
|
698
793
|
selfId
|
|
699
794
|
}
|
|
700
795
|
});
|
|
701
|
-
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`);
|
|
702
797
|
});
|
|
703
|
-
withGlobals(
|
|
704
|
-
|
|
798
|
+
withGlobals(mux.command("mode")).description("report the detected session-backend mode").action((opts) => {
|
|
799
|
+
const ctx = ctxOf(opts);
|
|
800
|
+
let mode = "none";
|
|
705
801
|
try {
|
|
706
|
-
|
|
802
|
+
mode = selectSessionAdapter(ctx.env ?? process.env).name;
|
|
707
803
|
} catch {}
|
|
708
804
|
emit(formatOf(opts), {
|
|
709
|
-
toon: toonObject({ mode
|
|
710
|
-
json: { mode
|
|
805
|
+
toon: toonObject({ mode }),
|
|
806
|
+
json: { mode }
|
|
711
807
|
});
|
|
712
808
|
});
|
|
713
|
-
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) => {
|
|
714
836
|
const res = migrateStore(new FileStore(resolve(opts.from)), new FileStore(resolve(opts.to)));
|
|
715
837
|
emit(formatOf(opts), {
|
|
716
838
|
toon: toonObject({
|
|
@@ -721,10 +843,51 @@ withGlobals(admin.command("migrate")).description("merge one hub root state into
|
|
|
721
843
|
json: res
|
|
722
844
|
});
|
|
723
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
|
+
});
|
|
724
887
|
defineSpawn(program.command("spawn"));
|
|
725
888
|
defineSend(program.command("send"));
|
|
726
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);
|
|
727
|
-
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);
|
|
728
891
|
withGlobals(program).action((opts) => {
|
|
729
892
|
const ctx = ctxOf(opts);
|
|
730
893
|
touch(ctx);
|
|
@@ -752,7 +915,7 @@ withGlobals(program).action((opts) => {
|
|
|
752
915
|
units
|
|
753
916
|
}
|
|
754
917
|
});
|
|
755
|
-
if (!id) nextStep("cyberlegion
|
|
918
|
+
if (!id) nextStep("cyberlegion unit register to join");
|
|
756
919
|
else if (unread > 0) nextStep("cyberlegion mail inbox --unread");
|
|
757
920
|
});
|
|
758
921
|
program.parseAsync(process.argv).catch((err) => {
|