cyberlegion 2.0.0 → 2.0.1
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 +6 -0
- package/dist/cli.mjs +68 -14
- package/dist/index.d.mts +25 -4
- package/dist/index.mjs +68 -14
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/readme.md +1 -1
- package/skills/dispatch-governance/SKILL.md +1 -1
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +15 -15
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +15 -15
- package/skills/legate/SKILL.md +1 -1
- package/skills/legate/scripts/cyberlegion.mjs +15 -15
- package/skills/manage-inbox/SKILL.md +1 -1
- package/skills/manage-inbox/scripts/cyberlegion.mjs +15 -15
- package/skills/relay-governance/SKILL.md +1 -1
- package/skills/relay-governance/scripts/cyberlegion.mjs +15 -15
- package/skills/subagent-backend-governance/SKILL.md +1 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +15 -15
package/.plugin/pins.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# cyberlegion
|
|
2
2
|
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7625b31: `unit prune` keeps an exited unit that only shares a worktree while it is the last record pointing at that worktree, the worktree is still on disk, and its work has not landed. Before, prune deleted it and left the work with nothing pointing at it. "Landed" is the worktree library's judgment (clean, and its branch landed on the default branch), exported as `landedWorktrees` for library callers to pass to `prune`; without it, `prune` keeps every such record.
|
|
8
|
+
|
|
3
9
|
## 2.0.0
|
|
4
10
|
|
|
5
11
|
### Major Changes
|
package/dist/cli.mjs
CHANGED
|
@@ -11163,10 +11163,13 @@ function terminalGone(ctx, rec, terminal, cache) {
|
|
|
11163
11163
|
* work from. Reconcile-culls against the current mux's live set first, then judges each remaining
|
|
11164
11164
|
* record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
|
|
11165
11165
|
* pane alone — a live or unprobeable pane keeps it whatever its last-seen — and a pane-less record by
|
|
11166
|
-
* the staleness timer, its only signal. An exited unit whose worktree is still on disk is
|
|
11167
|
-
* `unit close` owns removing that worktree
|
|
11166
|
+
* the staleness timer, its only signal. An exited unit whose own linked worktree is still on disk is
|
|
11167
|
+
* kept, since `unit close` owns removing that worktree. So is an exited unit that is the last record
|
|
11168
|
+
* pointing at a linked worktree it shares, while that worktree is on disk and its work has not landed
|
|
11169
|
+
* (`options.landed`): deleting it would leave that work with nothing pointing at it. Every other
|
|
11170
|
+
* exited unit is deleted with `deleteExited`.
|
|
11168
11171
|
*/
|
|
11169
|
-
function prune(ctx) {
|
|
11172
|
+
function prune(ctx, options = {}) {
|
|
11170
11173
|
const exec = ctx.exec ?? realExec;
|
|
11171
11174
|
const now = ctx.now?.() ?? Date.now();
|
|
11172
11175
|
const marked = reconcile(ctx);
|
|
@@ -11180,7 +11183,7 @@ function prune(ctx) {
|
|
|
11180
11183
|
marked.push(rec);
|
|
11181
11184
|
}
|
|
11182
11185
|
}
|
|
11183
|
-
const deleted = deleteExited(ctx);
|
|
11186
|
+
const deleted = deleteExited(ctx, options.landed ?? (() => false));
|
|
11184
11187
|
const gone = new Set(deleted.map((r) => r.id));
|
|
11185
11188
|
return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
|
|
11186
11189
|
...r,
|
|
@@ -11190,11 +11193,26 @@ function prune(ctx) {
|
|
|
11190
11193
|
pruned: "deleted"
|
|
11191
11194
|
}))];
|
|
11192
11195
|
}
|
|
11193
|
-
/** Whether
|
|
11194
|
-
*
|
|
11195
|
-
function
|
|
11196
|
-
|
|
11197
|
-
|
|
11196
|
+
/** Whether `checkout` is a linked worktree still on disk — something a record can point at. The
|
|
11197
|
+
* primary checkout always exists, but nothing removes it, so no record has to stay to point at it. */
|
|
11198
|
+
function linkedOnDisk(checkout) {
|
|
11199
|
+
return checkout?.root != null && existsSync(checkout.root) && !isPrimaryCheckout(checkout.root, checkout.primaryRoot);
|
|
11200
|
+
}
|
|
11201
|
+
/** Whether another record in the store still points at the worktree at `root`, owned or shared. */
|
|
11202
|
+
function pointedAtByOther(store, root, self) {
|
|
11203
|
+
const at = resolve(root);
|
|
11204
|
+
return listAgents(store).some((a) => a.id !== self && (a.worktree?.root != null && resolve(a.worktree.root) === at || a.sharedWorktree?.root != null && resolve(a.sharedWorktree.root) === at));
|
|
11205
|
+
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Whether deleting this exited record would orphan a worktree on disk. Its own linked worktree is kept
|
|
11208
|
+
* for `unit close` to remove. A worktree it only shares is kept while this record is the last one
|
|
11209
|
+
* pointing at it and its work has not landed — the owner's record is gone, so nothing else would
|
|
11210
|
+
* lead to that work.
|
|
11211
|
+
*/
|
|
11212
|
+
function keepsWorktree(store, rec, landed) {
|
|
11213
|
+
if (linkedOnDisk(rec.worktree)) return true;
|
|
11214
|
+
const shared = rec.sharedWorktree;
|
|
11215
|
+
return linkedOnDisk(shared) && !pointedAtByOther(store, shared.root, rec.id) && !landed(shared.root);
|
|
11198
11216
|
}
|
|
11199
11217
|
/** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
|
|
11200
11218
|
* `unit prune` share for "a seat on the main checkout, which nothing removes". */
|
|
@@ -11202,7 +11220,7 @@ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
|
|
|
11202
11220
|
return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
|
|
11203
11221
|
}
|
|
11204
11222
|
/**
|
|
11205
|
-
* Delete every exited unit
|
|
11223
|
+
* Delete every exited unit that leaves no work on disk (`keepsWorktree`): its record, brief, mailbox, and the pane
|
|
11206
11224
|
* pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
|
|
11207
11225
|
* stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
|
|
11208
11226
|
* resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
|
|
@@ -11211,11 +11229,11 @@ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
|
|
|
11211
11229
|
* `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
|
|
11212
11230
|
* someone else.
|
|
11213
11231
|
*/
|
|
11214
|
-
function deleteExited(ctx) {
|
|
11232
|
+
function deleteExited(ctx, landed) {
|
|
11215
11233
|
const exec = ctx.exec ?? realExec;
|
|
11216
11234
|
const deleted = [];
|
|
11217
11235
|
for (const rec of listAgents(ctx.store)) {
|
|
11218
|
-
if (rec.status !== "exited" || isSessionIndependent(rec) ||
|
|
11236
|
+
if (rec.status !== "exited" || isSessionIndependent(rec) || keepsWorktree(ctx.store, rec, landed)) continue;
|
|
11219
11237
|
const primaryRoot = rec.worktree?.primaryRoot;
|
|
11220
11238
|
if (primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
11221
11239
|
"-C",
|
|
@@ -13201,6 +13219,42 @@ function removeCursor(settings, event, canonical) {
|
|
|
13201
13219
|
else delete hooks[event];
|
|
13202
13220
|
return true;
|
|
13203
13221
|
}
|
|
13222
|
+
// src/landed.ts
|
|
13223
|
+
/**
|
|
13224
|
+
* Which worktrees a unit shares hold work that has landed, as the `landed` judgment `prune` takes.
|
|
13225
|
+
* It is the worktree library's own judgment, the one its `acquire` recycles a worktree by: the
|
|
13226
|
+
* checkout is clean and its branch has landed on the default branch. Asked once per repository
|
|
13227
|
+
* before prune's synchronous pass, for every shared worktree still on disk. A worktree git cannot
|
|
13228
|
+
* judge (detached, no default branch, a repository git does not know) does not count as landed, so
|
|
13229
|
+
* prune keeps the record that points at it.
|
|
13230
|
+
*/
|
|
13231
|
+
async function landedWorktrees(ctx) {
|
|
13232
|
+
const exec = asyncExec(ctx.exec ?? realExec);
|
|
13233
|
+
const roots = /* @__PURE__ */ new Map();
|
|
13234
|
+
for (const rec of listAgents(ctx.store)) {
|
|
13235
|
+
const shared = rec.sharedWorktree;
|
|
13236
|
+
if (shared?.root && existsSync(shared.root)) roots.set(shared.root, shared.primaryRoot);
|
|
13237
|
+
}
|
|
13238
|
+
const landed = /* @__PURE__ */ new Set();
|
|
13239
|
+
const asked = /* @__PURE__ */ new Set();
|
|
13240
|
+
for (const [root, recorded] of roots) {
|
|
13241
|
+
const primary = await primaryRoot({
|
|
13242
|
+
from: root,
|
|
13243
|
+
exec
|
|
13244
|
+
}).catch(() => recorded);
|
|
13245
|
+
if (!primary || asked.has(primary)) continue;
|
|
13246
|
+
asked.add(primary);
|
|
13247
|
+
const entries = await listWorktrees({
|
|
13248
|
+
primaryRoot: primary,
|
|
13249
|
+
exec,
|
|
13250
|
+
ignore: LEASE_IGNORE
|
|
13251
|
+
}).catch(() => []);
|
|
13252
|
+
for (const e of entries) if (e.linked && e.merged === true && e.dirty === false) landed.add(e.root);
|
|
13253
|
+
}
|
|
13254
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
13255
|
+
for (const root of roots.keys()) normalized.set(root, landed.has(await normalizeWorktreePath(root)));
|
|
13256
|
+
return (root) => normalized.get(root) ?? false;
|
|
13257
|
+
}
|
|
13204
13258
|
// src/output.ts
|
|
13205
13259
|
function stringifyCell(v) {
|
|
13206
13260
|
if (v == null) return "";
|
|
@@ -15016,10 +15070,10 @@ function runWho(opts) {
|
|
|
15016
15070
|
if (agents.length === 0) nextStep("cyberlegion unit register to join");
|
|
15017
15071
|
}
|
|
15018
15072
|
syncsUnits(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);
|
|
15019
|
-
syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and delete exited units
|
|
15073
|
+
syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and delete exited units that leave no work on disk").action(async (opts) => {
|
|
15020
15074
|
const ctx = ctxOf(opts);
|
|
15021
15075
|
touch(ctx);
|
|
15022
|
-
const changed = prune(ctx);
|
|
15076
|
+
const changed = prune(ctx, { landed: await landedWorktrees(ctx) });
|
|
15023
15077
|
emit(formatOf(opts), {
|
|
15024
15078
|
toon: toonList("pruned", changed, [
|
|
15025
15079
|
{
|
package/dist/index.d.mts
CHANGED
|
@@ -285,10 +285,22 @@ type PrunedRecord = AgentRecord & {
|
|
|
285
285
|
* work from. Reconcile-culls against the current mux's live set first, then judges each remaining
|
|
286
286
|
* record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
|
|
287
287
|
* pane alone — a live or unprobeable pane keeps it whatever its last-seen — and a pane-less record by
|
|
288
|
-
* the staleness timer, its only signal. An exited unit whose worktree is still on disk is
|
|
289
|
-
* `unit close` owns removing that worktree
|
|
288
|
+
* the staleness timer, its only signal. An exited unit whose own linked worktree is still on disk is
|
|
289
|
+
* kept, since `unit close` owns removing that worktree. So is an exited unit that is the last record
|
|
290
|
+
* pointing at a linked worktree it shares, while that worktree is on disk and its work has not landed
|
|
291
|
+
* (`options.landed`): deleting it would leave that work with nothing pointing at it. Every other
|
|
292
|
+
* exited unit is deleted with `deleteExited`.
|
|
290
293
|
*/
|
|
291
|
-
export declare function prune(ctx: IdContext): PrunedRecord[];
|
|
294
|
+
export declare function prune(ctx: IdContext, options?: PruneOptions): PrunedRecord[];
|
|
295
|
+
/** What `prune` is told from outside its synchronous pass. */
|
|
296
|
+
interface PruneOptions {
|
|
297
|
+
/**
|
|
298
|
+
* Whether the work in the linked worktree at `root` has landed. Absent, no worktree counts as
|
|
299
|
+
* landed, so prune keeps every record it would otherwise orphan work by deleting. `unit prune`
|
|
300
|
+
* passes `landedWorktrees`, the worktree library's judgment, asked before the pass.
|
|
301
|
+
*/
|
|
302
|
+
landed?: (root: string) => boolean;
|
|
303
|
+
}
|
|
292
304
|
interface AgentDef {
|
|
293
305
|
name: string;
|
|
294
306
|
description?: string;
|
|
@@ -431,6 +443,15 @@ interface InstallResult {
|
|
|
431
443
|
* plugin ships it (removing an earlier project copy), otherwise register the PATH-first project hook.
|
|
432
444
|
*/
|
|
433
445
|
export declare function install(harness: Harness, projectDir?: string, pin?: string): InstallResult[];
|
|
446
|
+
/**
|
|
447
|
+
* Which worktrees a unit shares hold work that has landed, as the `landed` judgment `prune` takes.
|
|
448
|
+
* It is the worktree library's own judgment, the one its `acquire` recycles a worktree by: the
|
|
449
|
+
* checkout is clean and its branch has landed on the default branch. Asked once per repository
|
|
450
|
+
* before prune's synchronous pass, for every shared worktree still on disk. A worktree git cannot
|
|
451
|
+
* judge (detached, no default branch, a repository git does not know) does not count as landed, so
|
|
452
|
+
* prune keeps the record that points at it.
|
|
453
|
+
*/
|
|
454
|
+
export declare function landedWorktrees(ctx: Pick<IdContext, 'store' | 'exec'>): Promise<(root: string) => boolean>;
|
|
434
455
|
interface MsgContext {
|
|
435
456
|
store: Store;
|
|
436
457
|
now?: () => number;
|
|
@@ -934,4 +955,4 @@ interface WatchOptions {
|
|
|
934
955
|
* `watch` prints stays unread and still surfaces later in `mail inbox`/`mail await`.
|
|
935
956
|
*/
|
|
936
957
|
export declare function watchMail(ctx: MsgContext, input: WatchInput, onMessage: (msg: InboxItem) => void, opts?: WatchOptions): Promise<void>;
|
|
937
|
-
export { type AcquireInput, type AcquireResult, type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, type DecommissionInput, type DecommissionResult, type Exec, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type OwnershipErrorCode, type ProjectContext, type ProjectRecord, type RegisterInput, type RootOptions, type SendInput, type ServiceContext, type ServiceHealth, type ServiceLease, type ServiceView, type SpawnInput, type SpawnResult, type StartInput, type StartResult, type Store, assertDistinctFromPrimary, gitWorktreeAdapter, probeMultiplexer, resolvePrimaryRoot };
|
|
958
|
+
export { type AcquireInput, type AcquireResult, type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, type DecommissionInput, type DecommissionResult, type Exec, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type OwnershipErrorCode, type ProjectContext, type ProjectRecord, type PruneOptions, type RegisterInput, type RootOptions, type SendInput, type ServiceContext, type ServiceHealth, type ServiceLease, type ServiceView, type SpawnInput, type SpawnResult, type StartInput, type StartResult, type Store, assertDistinctFromPrimary, gitWorktreeAdapter, probeMultiplexer, resolvePrimaryRoot };
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { callerPane, currentPane, herdrMuxAdapter, isStaged, nodeExec, probeMult
|
|
|
2
2
|
import { assertDistinctFromPrimary, assertDistinctFromPrimary as assertDistinctFromPrimary$1, gitWorktreeAdapter, gitWorktreeAdapter as gitWorktreeAdapter$1, resolvePrimaryRoot, resolvePrimaryRoot as resolvePrimaryRoot$1 } from "cyber-mux/worktree";
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
-
import { acquire, gitWorktreeCreator, primaryRoot, release } from "cyber-agentkit/worktrees";
|
|
5
|
+
import { acquire, gitWorktreeCreator, listWorktrees, normalizeWorktreePath, primaryRoot, release } from "cyber-agentkit/worktrees";
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -738,10 +738,13 @@ function terminalGone(ctx, rec, terminal, cache) {
|
|
|
738
738
|
* work from. Reconcile-culls against the current mux's live set first, then judges each remaining
|
|
739
739
|
* record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
|
|
740
740
|
* pane alone — a live or unprobeable pane keeps it whatever its last-seen — and a pane-less record by
|
|
741
|
-
* the staleness timer, its only signal. An exited unit whose worktree is still on disk is
|
|
742
|
-
* `unit close` owns removing that worktree
|
|
741
|
+
* the staleness timer, its only signal. An exited unit whose own linked worktree is still on disk is
|
|
742
|
+
* kept, since `unit close` owns removing that worktree. So is an exited unit that is the last record
|
|
743
|
+
* pointing at a linked worktree it shares, while that worktree is on disk and its work has not landed
|
|
744
|
+
* (`options.landed`): deleting it would leave that work with nothing pointing at it. Every other
|
|
745
|
+
* exited unit is deleted with `deleteExited`.
|
|
743
746
|
*/
|
|
744
|
-
function prune(ctx) {
|
|
747
|
+
function prune(ctx, options = {}) {
|
|
745
748
|
const exec = ctx.exec ?? realExec;
|
|
746
749
|
const now = ctx.now?.() ?? Date.now();
|
|
747
750
|
const marked = reconcile(ctx);
|
|
@@ -755,7 +758,7 @@ function prune(ctx) {
|
|
|
755
758
|
marked.push(rec);
|
|
756
759
|
}
|
|
757
760
|
}
|
|
758
|
-
const deleted = deleteExited(ctx);
|
|
761
|
+
const deleted = deleteExited(ctx, options.landed ?? (() => false));
|
|
759
762
|
const gone = new Set(deleted.map((r) => r.id));
|
|
760
763
|
return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
|
|
761
764
|
...r,
|
|
@@ -765,11 +768,26 @@ function prune(ctx) {
|
|
|
765
768
|
pruned: "deleted"
|
|
766
769
|
}))];
|
|
767
770
|
}
|
|
768
|
-
/** Whether
|
|
769
|
-
*
|
|
770
|
-
function
|
|
771
|
-
|
|
772
|
-
|
|
771
|
+
/** Whether `checkout` is a linked worktree still on disk — something a record can point at. The
|
|
772
|
+
* primary checkout always exists, but nothing removes it, so no record has to stay to point at it. */
|
|
773
|
+
function linkedOnDisk(checkout) {
|
|
774
|
+
return checkout?.root != null && existsSync(checkout.root) && !isPrimaryCheckout(checkout.root, checkout.primaryRoot);
|
|
775
|
+
}
|
|
776
|
+
/** Whether another record in the store still points at the worktree at `root`, owned or shared. */
|
|
777
|
+
function pointedAtByOther(store, root, self) {
|
|
778
|
+
const at = resolve(root);
|
|
779
|
+
return listAgents(store).some((a) => a.id !== self && (a.worktree?.root != null && resolve(a.worktree.root) === at || a.sharedWorktree?.root != null && resolve(a.sharedWorktree.root) === at));
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Whether deleting this exited record would orphan a worktree on disk. Its own linked worktree is kept
|
|
783
|
+
* for `unit close` to remove. A worktree it only shares is kept while this record is the last one
|
|
784
|
+
* pointing at it and its work has not landed — the owner's record is gone, so nothing else would
|
|
785
|
+
* lead to that work.
|
|
786
|
+
*/
|
|
787
|
+
function keepsWorktree(store, rec, landed) {
|
|
788
|
+
if (linkedOnDisk(rec.worktree)) return true;
|
|
789
|
+
const shared = rec.sharedWorktree;
|
|
790
|
+
return linkedOnDisk(shared) && !pointedAtByOther(store, shared.root, rec.id) && !landed(shared.root);
|
|
773
791
|
}
|
|
774
792
|
/** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
|
|
775
793
|
* `unit prune` share for "a seat on the main checkout, which nothing removes". */
|
|
@@ -777,7 +795,7 @@ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
|
|
|
777
795
|
return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
|
|
778
796
|
}
|
|
779
797
|
/**
|
|
780
|
-
* Delete every exited unit
|
|
798
|
+
* Delete every exited unit that leaves no work on disk (`keepsWorktree`): its record, brief, mailbox, and the pane
|
|
781
799
|
* pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
|
|
782
800
|
* stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
|
|
783
801
|
* resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
|
|
@@ -786,11 +804,11 @@ function isPrimaryCheckout(worktreeRoot, primaryRoot) {
|
|
|
786
804
|
* `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
|
|
787
805
|
* someone else.
|
|
788
806
|
*/
|
|
789
|
-
function deleteExited(ctx) {
|
|
807
|
+
function deleteExited(ctx, landed) {
|
|
790
808
|
const exec = ctx.exec ?? realExec;
|
|
791
809
|
const deleted = [];
|
|
792
810
|
for (const rec of listAgents(ctx.store)) {
|
|
793
|
-
if (rec.status !== "exited" || isSessionIndependent(rec) ||
|
|
811
|
+
if (rec.status !== "exited" || isSessionIndependent(rec) || keepsWorktree(ctx.store, rec, landed)) continue;
|
|
794
812
|
const primaryRoot = rec.worktree?.primaryRoot;
|
|
795
813
|
if (primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
796
814
|
"-C",
|
|
@@ -2371,6 +2389,42 @@ function removeCursor(settings, event, canonical) {
|
|
|
2371
2389
|
else delete hooks[event];
|
|
2372
2390
|
return true;
|
|
2373
2391
|
}
|
|
2392
|
+
// src/landed.ts
|
|
2393
|
+
/**
|
|
2394
|
+
* Which worktrees a unit shares hold work that has landed, as the `landed` judgment `prune` takes.
|
|
2395
|
+
* It is the worktree library's own judgment, the one its `acquire` recycles a worktree by: the
|
|
2396
|
+
* checkout is clean and its branch has landed on the default branch. Asked once per repository
|
|
2397
|
+
* before prune's synchronous pass, for every shared worktree still on disk. A worktree git cannot
|
|
2398
|
+
* judge (detached, no default branch, a repository git does not know) does not count as landed, so
|
|
2399
|
+
* prune keeps the record that points at it.
|
|
2400
|
+
*/
|
|
2401
|
+
async function landedWorktrees(ctx) {
|
|
2402
|
+
const exec = asyncExec(ctx.exec ?? realExec);
|
|
2403
|
+
const roots = /* @__PURE__ */ new Map();
|
|
2404
|
+
for (const rec of listAgents(ctx.store)) {
|
|
2405
|
+
const shared = rec.sharedWorktree;
|
|
2406
|
+
if (shared?.root && existsSync(shared.root)) roots.set(shared.root, shared.primaryRoot);
|
|
2407
|
+
}
|
|
2408
|
+
const landed = /* @__PURE__ */ new Set();
|
|
2409
|
+
const asked = /* @__PURE__ */ new Set();
|
|
2410
|
+
for (const [root, recorded] of roots) {
|
|
2411
|
+
const primary = await primaryRoot({
|
|
2412
|
+
from: root,
|
|
2413
|
+
exec
|
|
2414
|
+
}).catch(() => recorded);
|
|
2415
|
+
if (!primary || asked.has(primary)) continue;
|
|
2416
|
+
asked.add(primary);
|
|
2417
|
+
const entries = await listWorktrees({
|
|
2418
|
+
primaryRoot: primary,
|
|
2419
|
+
exec,
|
|
2420
|
+
ignore: LEASE_IGNORE
|
|
2421
|
+
}).catch(() => []);
|
|
2422
|
+
for (const e of entries) if (e.linked && e.merged === true && e.dirty === false) landed.add(e.root);
|
|
2423
|
+
}
|
|
2424
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
2425
|
+
for (const root of roots.keys()) normalized.set(root, landed.has(await normalizeWorktreePath(root)));
|
|
2426
|
+
return (root) => normalized.get(root) ?? false;
|
|
2427
|
+
}
|
|
2374
2428
|
// src/output.ts
|
|
2375
2429
|
function stringifyCell(v) {
|
|
2376
2430
|
if (v == null) return "";
|
|
@@ -3352,4 +3406,4 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
|
|
|
3352
3406
|
await sleep(POLL_MS);
|
|
3353
3407
|
}
|
|
3354
3408
|
}
|
|
3355
|
-
export { DEFAULT_RESERVATION_TTL_MS, FileStore, ServiceOwnershipError, ack, acquireService, assertDistinctFromPrimary, awaitReply, bindService, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, handoffService, inbox, injectInbox, install, listAgentDefs, listAgents, listProjects, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, registerProject, releaseService, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProject, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, resolveService, selectSessionAdapter, send, serviceEndpointId, spawn, spawnAndWake, startService, toonList, toonObject, touch, verifyOwnership, watchMail, withOwnership };
|
|
3409
|
+
export { DEFAULT_RESERVATION_TTL_MS, FileStore, ServiceOwnershipError, ack, acquireService, assertDistinctFromPrimary, awaitReply, bindService, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, handoffService, inbox, injectInbox, install, landedWorktrees, listAgentDefs, listAgents, listProjects, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, registerProject, releaseService, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProject, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, resolveService, selectSessionAdapter, send, serviceEndpointId, spawn, spawnAndWake, startService, toonList, toonObject, touch, verifyOwnership, watchMail, withOwnership };
|
package/package.json
CHANGED
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": "2.0.
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
package/readme.md
CHANGED
|
@@ -30,7 +30,7 @@ npx cyberlegion unit register --handle scout # register/refresh this sess
|
|
|
30
30
|
npx cyberlegion unit register --standing --handle homa # mint a standing, session-independent owner inbox
|
|
31
31
|
npx cyberlegion unit whoami # print your own identity
|
|
32
32
|
npx cyberlegion unit who # list addressable peers / live peer sessions
|
|
33
|
-
npx cyberlegion unit prune # mark dead agents exited; delete exited ones
|
|
33
|
+
npx cyberlegion unit prune # mark dead agents exited; delete exited ones that leave no work on disk
|
|
34
34
|
npx cyberlegion unit spawn --agent reviewer # launch a peer in its own git worktree (tmux/herdr)
|
|
35
35
|
npx cyberlegion unit focus <ref> # move input focus to a peer's session
|
|
36
36
|
npx cyberlegion unit nudge <ref> # ring a peer's session (doorbell)
|
|
@@ -18,7 +18,7 @@ caller supplies `P`, it is the definition to use; the by-name lookup is skipped.
|
|
|
18
18
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
19
19
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
20
20
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
21
|
-
> with the same arguments: `npx -y cyberlegion@2.0.
|
|
21
|
+
> with the same arguments: `npx -y cyberlegion@2.0.1`.
|
|
22
22
|
|
|
23
23
|
## 1. Resolve the agent definition
|
|
24
24
|
|