cyberlegion 1.7.0 → 1.9.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.plugin/pins.json +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/cli.mjs +159 -17
- package/dist/index.d.mts +13 -5
- package/dist/index.mjs +61 -11
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/skills/dispatch-governance/SKILL.md +1 -1
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +46 -45
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +46 -45
- package/skills/legate/SKILL.md +1 -1
- package/skills/legate/scripts/cyberlegion.mjs +46 -45
- package/skills/manage-inbox/SKILL.md +1 -1
- package/skills/manage-inbox/scripts/cyberlegion.mjs +46 -45
- package/skills/relay-governance/SKILL.md +1 -1
- package/skills/relay-governance/scripts/cyberlegion.mjs +46 -45
- package/skills/subagent-backend-governance/SKILL.md +1 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +46 -45
package/.plugin/pins.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# cyberlegion
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- deed34d: `mail inbox --unit <handle|id>` lists another unit's mail without acking it and without the caller taking that unit's identity. Exited units resolve too, so a cleanup sweep can check whether a dead unit left unread mail without reading the hub's files itself. The output carries a `mailbox` state of `none` (no mail was ever delivered), `present`, or `unreadable`. An unreadable mailbox exits 1, so a caller never reads it as empty.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- a7a2d57: `unit spawn` no longer leaves several unsent "Read your brief" lines in a new session's input box. The first-turn ring now waits for the harness to draw its input box before typing; keys typed while a harness boots are taken as one paste, so each Enter became a newline and re-typed copies piled up. Copies of the doorbell sitting in the input box are also no longer counted as a posted turn, so a staged doorbell is flushed with Enter instead of reported rung.
|
|
12
|
+
|
|
13
|
+
## 1.8.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 991f54c: `unit close` now closes a unit whose worktree is its repository's primary checkout instead of refusing it. It ends the session and deletes the record, pane pointer, brief and mailbox, and never removes, resets or cleans the checkout, so no `--force` is needed and uncommitted changes there are not refused. `--force` is still refused for such a unit, since it asks to discard changes in the checkout. This lets a live seat on a main checkout, or an exited record pointing at one, be removed for good.
|
|
18
|
+
- 999b959: Update runtime dependencies.
|
|
19
|
+
|
|
3
20
|
## 1.7.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/dist/cli.mjs
CHANGED
|
@@ -2963,7 +2963,7 @@ function useColor() {
|
|
|
2963
2963
|
}
|
|
2964
2964
|
new Command();
|
|
2965
2965
|
//#endregion
|
|
2966
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.
|
|
2966
|
+
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/exec-Bmdcg_T7.mjs
|
|
2967
2967
|
const nodeExec$1 = (cmd, args) => {
|
|
2968
2968
|
try {
|
|
2969
2969
|
const out = execFileSync(cmd, args, {
|
|
@@ -2991,7 +2991,7 @@ function withReason(exec, message) {
|
|
|
2991
2991
|
return exec.lastError ? `${message} — ${exec.lastError}` : message;
|
|
2992
2992
|
}
|
|
2993
2993
|
//#endregion
|
|
2994
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.
|
|
2994
|
+
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/backend-D3VSsev4.mjs
|
|
2995
2995
|
const KNOWN_MUX = [
|
|
2996
2996
|
"tmux",
|
|
2997
2997
|
"rmux",
|
|
@@ -10248,7 +10248,7 @@ async function exists(path) {
|
|
|
10248
10248
|
}
|
|
10249
10249
|
}
|
|
10250
10250
|
//#endregion
|
|
10251
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.
|
|
10251
|
+
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/worktree.mjs
|
|
10252
10252
|
/**
|
|
10253
10253
|
* This module's own refusals and failures — plain cyber-mux prose, never a dependency's raw words.
|
|
10254
10254
|
* `reportWorktreeFailure` (`cli.ts`) forwards a `WorktreeGitError`'s message onto stdout verbatim
|
|
@@ -10671,6 +10671,20 @@ function resolveOwnerMailbox(store, ref) {
|
|
|
10671
10671
|
if (services.length > 1) throw new Error(`"${ref}" names ${services.length} service endpoints — pass an endpoint id (${services.map((a) => a.id).join(", ")})`);
|
|
10672
10672
|
return resolveStandingOwner(store, ref);
|
|
10673
10673
|
}
|
|
10674
|
+
/** Resolve the unit whose mailbox `mail inbox --unit` reads, by id or handle. Unlike a recipient, an
|
|
10675
|
+
* exited unit resolves: the caller is deciding about that unit (a sweep asking whether a dead unit left
|
|
10676
|
+
* unread mail), not writing to it. A live holder of the handle wins; several exited holders and no live
|
|
10677
|
+
* one fail rather than picking one. */
|
|
10678
|
+
function resolveUnitMailbox(store, ref) {
|
|
10679
|
+
const byId = loadAgent(store, ref);
|
|
10680
|
+
if (byId) return byId;
|
|
10681
|
+
const { live, exited } = matchHandle(listAgents(store), ref);
|
|
10682
|
+
const byHandle = pickLive(ref, live);
|
|
10683
|
+
if (byHandle) return byHandle;
|
|
10684
|
+
if (exited.length > 1) throw new Error(`"${ref}" names ${exited.length} exited units — pass an id (${exited.map((a) => a.id).join(", ")})`);
|
|
10685
|
+
if (exited[0]) return exited[0];
|
|
10686
|
+
throw new Error(`no unit addressable as "${ref}" (tried id and handle)`);
|
|
10687
|
+
}
|
|
10674
10688
|
/**
|
|
10675
10689
|
* Bind the caller's own unit as a standing owner's presence — the live unit standing in for a
|
|
10676
10690
|
* durable record that has no session of its own. Order matters: resolve the standing record first
|
|
@@ -11016,6 +11030,10 @@ function readShape(rows, shape) {
|
|
|
11016
11030
|
}
|
|
11017
11031
|
return { kind: "unknown" };
|
|
11018
11032
|
}
|
|
11033
|
+
/** Whether this reader knows `harness`'s input box, so a missing box on screen means it is not drawn yet. */
|
|
11034
|
+
function knowsPromptOf(harness) {
|
|
11035
|
+
return harness !== void 0 && SHAPES[harness] !== void 0;
|
|
11036
|
+
}
|
|
11019
11037
|
/**
|
|
11020
11038
|
* Read the input box off `screen` — a plain-text scrape of the pane — with the shape of `harness`,
|
|
11021
11039
|
* or, when the harness is not known, with each known shape in turn until one recognizes the box.
|
|
@@ -11116,6 +11134,27 @@ function copiesOn(screen, message) {
|
|
|
11116
11134
|
return screen.split("\n").filter((row) => row.replace(/\s+/g, " ").includes(needle)).length;
|
|
11117
11135
|
}
|
|
11118
11136
|
/**
|
|
11137
|
+
* Read `screen` for the ring: whether `message` sits in the input box, and how many copies of it
|
|
11138
|
+
* are on screen outside the box. With the box read by its harness's shape, copies inside the box are
|
|
11139
|
+
* left out of the count: a booting harness can take several typed copies as one paste, and those
|
|
11140
|
+
* copies are staged text, not posted turns. A screen with no input box the reader knows falls back to
|
|
11141
|
+
* cyber-mux's `isStaged`, and the box's copy is counted with the rest.
|
|
11142
|
+
*/
|
|
11143
|
+
function readRing(screen, message, harness) {
|
|
11144
|
+
const box = screen ? readPrompt(screen, harness) : { kind: "unknown" };
|
|
11145
|
+
if (box.kind === "unknown") return {
|
|
11146
|
+
boxKnown: false,
|
|
11147
|
+
staged: isStaged(screen, message),
|
|
11148
|
+
outside: copiesOn(screen, message)
|
|
11149
|
+
};
|
|
11150
|
+
const inBox = box.kind === "draft" ? copiesOn(box.text, message) : 0;
|
|
11151
|
+
return {
|
|
11152
|
+
boxKnown: true,
|
|
11153
|
+
staged: inBox > 0,
|
|
11154
|
+
outside: copiesOn(screen, message) - inBox
|
|
11155
|
+
};
|
|
11156
|
+
}
|
|
11157
|
+
/**
|
|
11119
11158
|
* Submit `message` to `target` as a taken turn: cyber-mux's `nudge`, with one more way to see that the
|
|
11120
11159
|
* turn was taken.
|
|
11121
11160
|
*
|
|
@@ -11135,18 +11174,32 @@ function copiesOn(screen, message) {
|
|
|
11135
11174
|
* queue. Text that is neither staged nor posted is lost: after one more settle for a slow redraw, it
|
|
11136
11175
|
* is typed again. It is opt-in because it needs the posted copy to stay on screen: a busy peer's
|
|
11137
11176
|
* output can scroll earlier copies away, so a ring to it could read as lost and be typed twice.
|
|
11177
|
+
*
|
|
11178
|
+
* With `requirePosted` and a harness whose input box this reader knows, nothing is typed until the
|
|
11179
|
+
* harness has drawn that box, for up to the
|
|
11180
|
+
* whole retry budget. Keys typed while a harness boots are buffered, and the harness takes them as one
|
|
11181
|
+
* paste once it draws the box: each Enter becomes a newline, so re-typed copies pile up in the box
|
|
11182
|
+
* unsent. A box that never shows within the budget is typed into anyway, as before.
|
|
11183
|
+
*
|
|
11184
|
+
* Where the input box can be read, copies of the text inside it are not counted as posted: only a
|
|
11185
|
+
* copy outside the box means the harness took the turn.
|
|
11138
11186
|
*/
|
|
11139
11187
|
async function ringTurn(adapter, exec, target, message, opts = {}) {
|
|
11140
11188
|
const attempts = opts.attempts ?? DEFAULT_ATTEMPTS;
|
|
11141
11189
|
const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
11142
11190
|
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
11143
11191
|
if (!adapter.paneExists(exec, target)) throw new Error(`nudge failed: pane ${target.id} no longer exists — the peer's session is gone, not busy.`);
|
|
11144
|
-
|
|
11192
|
+
let first = adapter.read(exec, target).text;
|
|
11193
|
+
if (opts.requirePosted && knowsPromptOf(opts.harness)) for (let wait = 0; wait < attempts && !readRing(first, message, opts.harness).boxKnown; wait++) {
|
|
11194
|
+
await sleep(settleMs);
|
|
11195
|
+
first = adapter.read(exec, target).text;
|
|
11196
|
+
}
|
|
11197
|
+
const before = readRing(first, message, opts.harness).outside;
|
|
11145
11198
|
const check = () => {
|
|
11146
|
-
const
|
|
11147
|
-
const
|
|
11148
|
-
if (
|
|
11149
|
-
return
|
|
11199
|
+
const seen = readRing(adapter.read(exec, target).text, message, opts.harness);
|
|
11200
|
+
const posted = seen.outside >= before + (seen.boxKnown || !seen.staged ? 1 : 2);
|
|
11201
|
+
if (seen.staged) return posted ? "taken" : "staged";
|
|
11202
|
+
return posted || !opts.requirePosted ? "taken" : "lost";
|
|
11150
11203
|
};
|
|
11151
11204
|
adapter.submit(exec, target, message);
|
|
11152
11205
|
await sleep(settleMs);
|
|
@@ -11306,7 +11359,10 @@ async function wakeRecipient(store, getAdapter, exec, input, nudgeOpts, guardOpt
|
|
|
11306
11359
|
try {
|
|
11307
11360
|
const adapter = getAdapter();
|
|
11308
11361
|
const target = { id: pane };
|
|
11309
|
-
await withDraftGuard(adapter, exec, target, () => ringTurn(adapter, exec, target, DELIVERY_DOORBELL,
|
|
11362
|
+
await withDraftGuard(adapter, exec, target, () => ringTurn(adapter, exec, target, DELIVERY_DOORBELL, {
|
|
11363
|
+
...nudgeOpts,
|
|
11364
|
+
harness
|
|
11365
|
+
}), {
|
|
11310
11366
|
...guardOpts,
|
|
11311
11367
|
harness,
|
|
11312
11368
|
ownText: (text) => isRingText(text)
|
|
@@ -11351,7 +11407,8 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
|
|
|
11351
11407
|
const doorbell = (input.resumed ? resumeDoorbell : spawnDoorbell)(input.briefPath);
|
|
11352
11408
|
await withDraftGuard(adapter, exec, input.target, () => ringTurn(adapter, exec, input.target, doorbell, {
|
|
11353
11409
|
...nudgeOpts,
|
|
11354
|
-
requirePosted: true
|
|
11410
|
+
requirePosted: true,
|
|
11411
|
+
harness: input.harness
|
|
11355
11412
|
}), {
|
|
11356
11413
|
...guardOpts,
|
|
11357
11414
|
harness: input.harness,
|
|
@@ -12575,6 +12632,26 @@ function inbox(ctx, q) {
|
|
|
12575
12632
|
if (q.thread) items = items.filter((m) => m.thread === q.thread);
|
|
12576
12633
|
return items.sort((a, b) => a.id.localeCompare(b.id));
|
|
12577
12634
|
}
|
|
12635
|
+
/** List another unit's mail read-only, for a caller deciding about that unit (a cleanup sweep): never
|
|
12636
|
+
* acks, and reports a missing or unreadable mailbox as its own state rather than as an empty one. */
|
|
12637
|
+
function unitInbox(ctx, q) {
|
|
12638
|
+
if (!ctx.store.hasMailbox(q.meId)) return {
|
|
12639
|
+
mailbox: "none",
|
|
12640
|
+
messages: []
|
|
12641
|
+
};
|
|
12642
|
+
try {
|
|
12643
|
+
return {
|
|
12644
|
+
mailbox: "present",
|
|
12645
|
+
messages: inbox(ctx, q)
|
|
12646
|
+
};
|
|
12647
|
+
} catch (err) {
|
|
12648
|
+
return {
|
|
12649
|
+
mailbox: "unreadable",
|
|
12650
|
+
messages: [],
|
|
12651
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12652
|
+
};
|
|
12653
|
+
}
|
|
12654
|
+
}
|
|
12578
12655
|
/** Peek at a message (unread or already-acked) without changing its state. */
|
|
12579
12656
|
function peek(ctx, meId, msgId) {
|
|
12580
12657
|
const snap = ctx.store.listInbox(meId);
|
|
@@ -12612,8 +12689,7 @@ function deleteMailbox(store, id) {
|
|
|
12612
12689
|
//#endregion
|
|
12613
12690
|
//#region src/decommission.ts
|
|
12614
12691
|
/**
|
|
12615
|
-
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses
|
|
12616
|
-
* the primary checkout (absolute — neither `--force` nor `--keep-worktree` overrides it) and a
|
|
12692
|
+
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
|
|
12617
12693
|
* dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
|
|
12618
12694
|
* is tolerated, but a genuine worktree-removal failure aborts and leaves the record intact so the
|
|
12619
12695
|
* operation is retryable.
|
|
@@ -12630,6 +12706,12 @@ function deleteMailbox(store, id) {
|
|
|
12630
12706
|
* discarded, so neither the dirty refusal nor `--force` applies — a dirty worktree is skipped by the
|
|
12631
12707
|
* next `acquire` rather than lost. Release precedes the pane teardown, so a release that throws leaves
|
|
12632
12708
|
* everything for a retry, as a failed removal does.
|
|
12709
|
+
*
|
|
12710
|
+
* A unit whose worktree is its repository's primary checkout — a seat registered on a main checkout —
|
|
12711
|
+
* is closed without touching the checkout: its session ends and its record, brief and mailbox are
|
|
12712
|
+
* reaped, but nothing is removed, reset or cleaned, so no dirty check applies and no `--force` is
|
|
12713
|
+
* needed. `--force` is refused there, since it asks to discard changes in the checkout. The checkout
|
|
12714
|
+
* is not reported as retained: it is no pool path a next unit could be spawned into.
|
|
12633
12715
|
*/
|
|
12634
12716
|
async function decommission(ctx, input) {
|
|
12635
12717
|
const rec = loadAgent(ctx.store, input.id);
|
|
@@ -12641,8 +12723,9 @@ async function decommission(ctx, input) {
|
|
|
12641
12723
|
const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
|
|
12642
12724
|
const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
|
|
12643
12725
|
const held = rec.worktree?.lease;
|
|
12644
|
-
|
|
12645
|
-
|
|
12726
|
+
const onPrimary = worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
|
|
12727
|
+
if (onPrimary && input.force) throw new Error(`refusing to close "${input.id}" with --force — its worktree is the primary checkout, which close never alters; close it without --force to end its session and reap its record`);
|
|
12728
|
+
const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
|
|
12646
12729
|
if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
|
|
12647
12730
|
const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
|
|
12648
12731
|
if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
@@ -12671,7 +12754,7 @@ async function decommission(ctx, input) {
|
|
|
12671
12754
|
return {
|
|
12672
12755
|
agent: rec,
|
|
12673
12756
|
worktreeRoot,
|
|
12674
|
-
retainedWorktree: worktreeExists && (input.keepWorktree || held) ? worktreeRoot : void 0,
|
|
12757
|
+
retainedWorktree: worktreeExists && !onPrimary && (input.keepWorktree || held) ? worktreeRoot : void 0,
|
|
12675
12758
|
...lease ? { lease } : {},
|
|
12676
12759
|
pane
|
|
12677
12760
|
};
|
|
@@ -14002,6 +14085,10 @@ var FileStore = class {
|
|
|
14002
14085
|
read: read ? readMessages(read) : []
|
|
14003
14086
|
};
|
|
14004
14087
|
}
|
|
14088
|
+
hasMailbox(id) {
|
|
14089
|
+
const dir = readPathOrUndefined(() => paths.inboxDir(this.root, id));
|
|
14090
|
+
return dir !== void 0 && existsSync(dir);
|
|
14091
|
+
}
|
|
14005
14092
|
ackMessage(id, msgId) {
|
|
14006
14093
|
const src = readPathOrUndefined(() => paths.messageFile(this.root, id, msgId));
|
|
14007
14094
|
if (!src || !existsSync(src)) throw new Error(`"${msgId}" is not an unread message in this inbox`);
|
|
@@ -14785,7 +14872,7 @@ function defineSpawn(cmd) {
|
|
|
14785
14872
|
});
|
|
14786
14873
|
}
|
|
14787
14874
|
defineSpawn(unit.command("spawn"));
|
|
14788
|
-
syncsUnits(withGlobals(unit.command("close"))).description("release the unit's worktree lease (or remove a worktree it holds no lease on), tear down its 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 a worktree close removes (
|
|
14875
|
+
syncsUnits(withGlobals(unit.command("close"))).description("release the unit's worktree lease (or remove a worktree it holds no lease on), tear down its 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 a worktree close removes (refused for a unit on the primary checkout)").option("--keep-worktree", "leave the worktree on disk for reuse and reap everything else (skips the dirty check)").action(async (ref, opts) => {
|
|
14789
14876
|
const ctx = ctxOf(opts);
|
|
14790
14877
|
touch(ctx);
|
|
14791
14878
|
const agent = resolveAgent(ctx.store, ref);
|
|
@@ -15232,12 +15319,67 @@ function emitInbox(ctx, opts, meId, readCmd) {
|
|
|
15232
15319
|
const firstUnread = items.find((m) => !m.read);
|
|
15233
15320
|
if (firstUnread) nextStep(`${readCmd} ${firstUnread.id}`);
|
|
15234
15321
|
}
|
|
15322
|
+
/** List another unit's mailbox read-only (`--unit`). Its `mailbox` state tells a missing mailbox
|
|
15323
|
+
* from an empty one; an unreadable one is reported on stdout and exits 1, never read as empty. */
|
|
15324
|
+
function emitUnitInbox(ctx, opts, ref) {
|
|
15325
|
+
let unit;
|
|
15326
|
+
try {
|
|
15327
|
+
unit = resolveUnitMailbox(ctx.store, ref);
|
|
15328
|
+
} catch (err) {
|
|
15329
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
15330
|
+
}
|
|
15331
|
+
const result = unitInbox({ store: ctx.store }, {
|
|
15332
|
+
meId: unit.id,
|
|
15333
|
+
unread: opts.unread,
|
|
15334
|
+
from: opts.from,
|
|
15335
|
+
thread: opts.thread
|
|
15336
|
+
});
|
|
15337
|
+
const unreadCount = result.messages.filter((m) => !m.read).length;
|
|
15338
|
+
const summary = result.mailbox === "none" ? "no mailbox" : result.mailbox === "unreadable" ? `mailbox unreadable: ${result.error}` : `${result.messages.length} messages (${unreadCount} unread)`;
|
|
15339
|
+
emit(formatOf(opts), {
|
|
15340
|
+
toon: [toonObject({
|
|
15341
|
+
unit: unit.id,
|
|
15342
|
+
handle: unit.handle,
|
|
15343
|
+
mailbox: result.mailbox
|
|
15344
|
+
}), toonList("messages", result.messages, [
|
|
15345
|
+
{
|
|
15346
|
+
key: "id",
|
|
15347
|
+
get: (m) => m.id
|
|
15348
|
+
},
|
|
15349
|
+
{
|
|
15350
|
+
key: "from",
|
|
15351
|
+
get: (m) => m.fromHandle
|
|
15352
|
+
},
|
|
15353
|
+
{
|
|
15354
|
+
key: "subject",
|
|
15355
|
+
get: (m) => m.subject ?? ""
|
|
15356
|
+
},
|
|
15357
|
+
{
|
|
15358
|
+
key: "read",
|
|
15359
|
+
get: (m) => m.read
|
|
15360
|
+
}
|
|
15361
|
+
], summary)].join("\n"),
|
|
15362
|
+
json: {
|
|
15363
|
+
unit: unit.id,
|
|
15364
|
+
handle: unit.handle,
|
|
15365
|
+
...result
|
|
15366
|
+
}
|
|
15367
|
+
});
|
|
15368
|
+
if (result.mailbox === "unreadable") {
|
|
15369
|
+
console.error(JSON.stringify({ error: `mailbox of ${unit.id} is unreadable: ${result.error}` }));
|
|
15370
|
+
process.exitCode = 1;
|
|
15371
|
+
}
|
|
15372
|
+
}
|
|
15235
15373
|
function runInbox(opts) {
|
|
15236
15374
|
const ctx = ctxOf(opts);
|
|
15237
15375
|
touch(ctx);
|
|
15376
|
+
if (opts.unit) {
|
|
15377
|
+
emitUnitInbox(ctx, opts, opts.unit);
|
|
15378
|
+
return;
|
|
15379
|
+
}
|
|
15238
15380
|
emitInbox(ctx, opts, opts.owner ? resolveOwnerMailbox(ctx.store, opts.owner) : requireSelf(ctx), "cyberlegion mail read");
|
|
15239
15381
|
}
|
|
15240
|
-
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);
|
|
15382
|
+
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").addOption(new Option("--unit <handle|id>", "list another unit's mailbox read-only (exited units too); never acks, reports mailbox: none|present|unreadable").conflicts("owner")).action(runInbox);
|
|
15241
15383
|
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) => {
|
|
15242
15384
|
const ctx = ctxOf(opts);
|
|
15243
15385
|
const meId = opts.owner ? resolveOwnerMailbox(ctx.store, opts.owner) : requireSelf(ctx);
|
package/dist/index.d.mts
CHANGED
|
@@ -162,6 +162,8 @@ interface MailboxStore {
|
|
|
162
162
|
putMessage(toId: string, msg: Message): void;
|
|
163
163
|
/** The full unread/read split for one agent's inbox. */
|
|
164
164
|
listInbox(id: string): InboxSnapshot;
|
|
165
|
+
/** Whether `id` has a mailbox at all — one exists once mail was first delivered to it. */
|
|
166
|
+
hasMailbox(id: string): boolean;
|
|
165
167
|
/** Ack a message by moving it out of the unread set; throws if it is not currently unread. */
|
|
166
168
|
ackMessage(id: string, msgId: string): Message;
|
|
167
169
|
/** Permanently remove a message (unread or already-acked) from `id`'s inbox; throws if absent. */
|
|
@@ -332,12 +334,12 @@ export declare function realizeLaunch(def: AgentDef, opts?: RealizeLaunchOptions
|
|
|
332
334
|
//#region src/decommission.d.ts
|
|
333
335
|
interface DecommissionInput {
|
|
334
336
|
id: string;
|
|
335
|
-
/** Discard uncommitted changes in the worktree.
|
|
337
|
+
/** Discard uncommitted changes in the worktree. Refused for a unit on the primary checkout, which close never alters. */
|
|
336
338
|
force?: boolean;
|
|
337
339
|
/**
|
|
338
340
|
* Reap the unit but leave its worktree on disk, so a pool can detach it and hand it to the next
|
|
339
|
-
* unit — far cheaper than a fresh checkout per unit.
|
|
340
|
-
*
|
|
341
|
+
* unit — far cheaper than a fresh checkout per unit. A unit on the primary checkout is closed this
|
|
342
|
+
* way with or without the flag.
|
|
341
343
|
*/
|
|
342
344
|
keepWorktree?: boolean;
|
|
343
345
|
}
|
|
@@ -356,8 +358,7 @@ interface DecommissionResult {
|
|
|
356
358
|
pane?: string;
|
|
357
359
|
}
|
|
358
360
|
/**
|
|
359
|
-
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses
|
|
360
|
-
* the primary checkout (absolute — neither `--force` nor `--keep-worktree` overrides it) and a
|
|
361
|
+
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
|
|
361
362
|
* dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
|
|
362
363
|
* is tolerated, but a genuine worktree-removal failure aborts and leaves the record intact so the
|
|
363
364
|
* operation is retryable.
|
|
@@ -374,6 +375,12 @@ interface DecommissionResult {
|
|
|
374
375
|
* discarded, so neither the dirty refusal nor `--force` applies — a dirty worktree is skipped by the
|
|
375
376
|
* next `acquire` rather than lost. Release precedes the pane teardown, so a release that throws leaves
|
|
376
377
|
* everything for a retry, as a failed removal does.
|
|
378
|
+
*
|
|
379
|
+
* A unit whose worktree is its repository's primary checkout — a seat registered on a main checkout —
|
|
380
|
+
* is closed without touching the checkout: its session ends and its record, brief and mailbox are
|
|
381
|
+
* reaped, but nothing is removed, reset or cleaned, so no dirty check applies and no `--force` is
|
|
382
|
+
* needed. `--force` is refused there, since it asks to discard changes in the checkout. The checkout
|
|
383
|
+
* is not reported as retained: it is no pool path a next unit could be spawned into.
|
|
377
384
|
*/
|
|
378
385
|
export declare function decommission(ctx: IdContext, input: DecommissionInput): Promise<DecommissionResult>;
|
|
379
386
|
//#endregion
|
|
@@ -859,6 +866,7 @@ export declare class FileStore implements Store {
|
|
|
859
866
|
ensureMarker(): void;
|
|
860
867
|
putMessage(toId: string, msg: Message): void;
|
|
861
868
|
listInbox(id: string): InboxSnapshot;
|
|
869
|
+
hasMailbox(id: string): boolean;
|
|
862
870
|
ackMessage(id: string, msgId: string): Message;
|
|
863
871
|
removeMessage(id: string, msgId: string): void;
|
|
864
872
|
removeMailbox(id: string): void;
|
package/dist/index.mjs
CHANGED
|
@@ -599,6 +599,10 @@ function readShape(rows, shape) {
|
|
|
599
599
|
}
|
|
600
600
|
return { kind: "unknown" };
|
|
601
601
|
}
|
|
602
|
+
/** Whether this reader knows `harness`'s input box, so a missing box on screen means it is not drawn yet. */
|
|
603
|
+
function knowsPromptOf(harness) {
|
|
604
|
+
return harness !== void 0 && SHAPES[harness] !== void 0;
|
|
605
|
+
}
|
|
602
606
|
/**
|
|
603
607
|
* Read the input box off `screen` — a plain-text scrape of the pane — with the shape of `harness`,
|
|
604
608
|
* or, when the harness is not known, with each known shape in turn until one recognizes the box.
|
|
@@ -699,6 +703,27 @@ function copiesOn(screen, message) {
|
|
|
699
703
|
return screen.split("\n").filter((row) => row.replace(/\s+/g, " ").includes(needle)).length;
|
|
700
704
|
}
|
|
701
705
|
/**
|
|
706
|
+
* Read `screen` for the ring: whether `message` sits in the input box, and how many copies of it
|
|
707
|
+
* are on screen outside the box. With the box read by its harness's shape, copies inside the box are
|
|
708
|
+
* left out of the count: a booting harness can take several typed copies as one paste, and those
|
|
709
|
+
* copies are staged text, not posted turns. A screen with no input box the reader knows falls back to
|
|
710
|
+
* cyber-mux's `isStaged`, and the box's copy is counted with the rest.
|
|
711
|
+
*/
|
|
712
|
+
function readRing(screen, message, harness) {
|
|
713
|
+
const box = screen ? readPrompt(screen, harness) : { kind: "unknown" };
|
|
714
|
+
if (box.kind === "unknown") return {
|
|
715
|
+
boxKnown: false,
|
|
716
|
+
staged: isStaged(screen, message),
|
|
717
|
+
outside: copiesOn(screen, message)
|
|
718
|
+
};
|
|
719
|
+
const inBox = box.kind === "draft" ? copiesOn(box.text, message) : 0;
|
|
720
|
+
return {
|
|
721
|
+
boxKnown: true,
|
|
722
|
+
staged: inBox > 0,
|
|
723
|
+
outside: copiesOn(screen, message) - inBox
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
702
727
|
* Submit `message` to `target` as a taken turn: cyber-mux's `nudge`, with one more way to see that the
|
|
703
728
|
* turn was taken.
|
|
704
729
|
*
|
|
@@ -718,18 +743,32 @@ function copiesOn(screen, message) {
|
|
|
718
743
|
* queue. Text that is neither staged nor posted is lost: after one more settle for a slow redraw, it
|
|
719
744
|
* is typed again. It is opt-in because it needs the posted copy to stay on screen: a busy peer's
|
|
720
745
|
* output can scroll earlier copies away, so a ring to it could read as lost and be typed twice.
|
|
746
|
+
*
|
|
747
|
+
* With `requirePosted` and a harness whose input box this reader knows, nothing is typed until the
|
|
748
|
+
* harness has drawn that box, for up to the
|
|
749
|
+
* whole retry budget. Keys typed while a harness boots are buffered, and the harness takes them as one
|
|
750
|
+
* paste once it draws the box: each Enter becomes a newline, so re-typed copies pile up in the box
|
|
751
|
+
* unsent. A box that never shows within the budget is typed into anyway, as before.
|
|
752
|
+
*
|
|
753
|
+
* Where the input box can be read, copies of the text inside it are not counted as posted: only a
|
|
754
|
+
* copy outside the box means the harness took the turn.
|
|
721
755
|
*/
|
|
722
756
|
async function ringTurn(adapter, exec, target, message, opts = {}) {
|
|
723
757
|
const attempts = opts.attempts ?? DEFAULT_ATTEMPTS;
|
|
724
758
|
const settleMs = opts.settleMs ?? DEFAULT_SETTLE_MS;
|
|
725
759
|
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
726
760
|
if (!adapter.paneExists(exec, target)) throw new Error(`nudge failed: pane ${target.id} no longer exists — the peer's session is gone, not busy.`);
|
|
727
|
-
|
|
761
|
+
let first = adapter.read(exec, target).text;
|
|
762
|
+
if (opts.requirePosted && knowsPromptOf(opts.harness)) for (let wait = 0; wait < attempts && !readRing(first, message, opts.harness).boxKnown; wait++) {
|
|
763
|
+
await sleep(settleMs);
|
|
764
|
+
first = adapter.read(exec, target).text;
|
|
765
|
+
}
|
|
766
|
+
const before = readRing(first, message, opts.harness).outside;
|
|
728
767
|
const check = () => {
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
return
|
|
768
|
+
const seen = readRing(adapter.read(exec, target).text, message, opts.harness);
|
|
769
|
+
const posted = seen.outside >= before + (seen.boxKnown || !seen.staged ? 1 : 2);
|
|
770
|
+
if (seen.staged) return posted ? "taken" : "staged";
|
|
771
|
+
return posted || !opts.requirePosted ? "taken" : "lost";
|
|
733
772
|
};
|
|
734
773
|
adapter.submit(exec, target, message);
|
|
735
774
|
await sleep(settleMs);
|
|
@@ -829,7 +868,8 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
|
|
|
829
868
|
const doorbell = (input.resumed ? resumeDoorbell : spawnDoorbell)(input.briefPath);
|
|
830
869
|
await withDraftGuard(adapter, exec, input.target, () => ringTurn(adapter, exec, input.target, doorbell, {
|
|
831
870
|
...nudgeOpts,
|
|
832
|
-
requirePosted: true
|
|
871
|
+
requirePosted: true,
|
|
872
|
+
harness: input.harness
|
|
833
873
|
}), {
|
|
834
874
|
...guardOpts,
|
|
835
875
|
harness: input.harness,
|
|
@@ -1810,8 +1850,7 @@ function deleteMailbox(store, id) {
|
|
|
1810
1850
|
//#endregion
|
|
1811
1851
|
//#region src/decommission.ts
|
|
1812
1852
|
/**
|
|
1813
|
-
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses
|
|
1814
|
-
* the primary checkout (absolute — neither `--force` nor `--keep-worktree` overrides it) and a
|
|
1853
|
+
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
|
|
1815
1854
|
* dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
|
|
1816
1855
|
* is tolerated, but a genuine worktree-removal failure aborts and leaves the record intact so the
|
|
1817
1856
|
* operation is retryable.
|
|
@@ -1828,6 +1867,12 @@ function deleteMailbox(store, id) {
|
|
|
1828
1867
|
* discarded, so neither the dirty refusal nor `--force` applies — a dirty worktree is skipped by the
|
|
1829
1868
|
* next `acquire` rather than lost. Release precedes the pane teardown, so a release that throws leaves
|
|
1830
1869
|
* everything for a retry, as a failed removal does.
|
|
1870
|
+
*
|
|
1871
|
+
* A unit whose worktree is its repository's primary checkout — a seat registered on a main checkout —
|
|
1872
|
+
* is closed without touching the checkout: its session ends and its record, brief and mailbox are
|
|
1873
|
+
* reaped, but nothing is removed, reset or cleaned, so no dirty check applies and no `--force` is
|
|
1874
|
+
* needed. `--force` is refused there, since it asks to discard changes in the checkout. The checkout
|
|
1875
|
+
* is not reported as retained: it is no pool path a next unit could be spawned into.
|
|
1831
1876
|
*/
|
|
1832
1877
|
async function decommission(ctx, input) {
|
|
1833
1878
|
const rec = loadAgent(ctx.store, input.id);
|
|
@@ -1839,8 +1884,9 @@ async function decommission(ctx, input) {
|
|
|
1839
1884
|
const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
|
|
1840
1885
|
const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
|
|
1841
1886
|
const held = rec.worktree?.lease;
|
|
1842
|
-
|
|
1843
|
-
|
|
1887
|
+
const onPrimary = worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
|
|
1888
|
+
if (onPrimary && input.force) throw new Error(`refusing to close "${input.id}" with --force — its worktree is the primary checkout, which close never alters; close it without --force to end its session and reap its record`);
|
|
1889
|
+
const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
|
|
1844
1890
|
if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
|
|
1845
1891
|
const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
|
|
1846
1892
|
if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
@@ -1869,7 +1915,7 @@ async function decommission(ctx, input) {
|
|
|
1869
1915
|
return {
|
|
1870
1916
|
agent: rec,
|
|
1871
1917
|
worktreeRoot,
|
|
1872
|
-
retainedWorktree: worktreeExists && (input.keepWorktree || held) ? worktreeRoot : void 0,
|
|
1918
|
+
retainedWorktree: worktreeExists && !onPrimary && (input.keepWorktree || held) ? worktreeRoot : void 0,
|
|
1873
1919
|
...lease ? { lease } : {},
|
|
1874
1920
|
pane
|
|
1875
1921
|
};
|
|
@@ -2851,6 +2897,10 @@ var FileStore = class {
|
|
|
2851
2897
|
read: read ? readMessages(read) : []
|
|
2852
2898
|
};
|
|
2853
2899
|
}
|
|
2900
|
+
hasMailbox(id) {
|
|
2901
|
+
const dir = readPathOrUndefined(() => paths.inboxDir(this.root, id));
|
|
2902
|
+
return dir !== void 0 && existsSync(dir);
|
|
2903
|
+
}
|
|
2854
2904
|
ackMessage(id, msgId) {
|
|
2855
2905
|
const src = readPathOrUndefined(() => paths.messageFile(this.root, id, msgId));
|
|
2856
2906
|
if (!src || !existsSync(src)) throw new Error(`"${msgId}" is not an unread message in this inbox`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cyberlegion",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Harness-agnostic agent session spawning and messaging over the filesystem",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"cyber-agentkit": "^2.0.0",
|
|
45
|
-
"cyber-mux": "0.
|
|
45
|
+
"cyber-mux": "0.12.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"cynapse": "^0.1.0"
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "cyberlegion",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.9.0",
|
|
5
5
|
"description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
|
@@ -17,7 +17,7 @@ satisfies verdict schema `V`.
|
|
|
17
17
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
18
18
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
19
19
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
20
|
-
> with the same arguments: `npx -y cyberlegion@1.
|
|
20
|
+
> with the same arguments: `npx -y cyberlegion@1.9.0`.
|
|
21
21
|
|
|
22
22
|
## 1. Resolve the agent definition
|
|
23
23
|
|