cyberlegion 1.1.0 → 1.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.plugin/pins.json +1 -1
- package/dist/cli.mjs +73 -27
- package/dist/index.d.mts +11 -5
- package/dist/index.mjs +72 -26
- package/hooks/hooks.json +0 -11
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/dispatch-governance/SKILL.md +1 -1
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +1 -1
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +1 -1
- package/skills/legate/SKILL.md +1 -1
- package/skills/legate/scripts/cyberlegion.mjs +1 -1
- package/skills/manage-inbox/SKILL.md +1 -1
- package/skills/manage-inbox/scripts/cyberlegion.mjs +1 -1
- package/skills/relay-governance/SKILL.md +1 -1
- package/skills/relay-governance/scripts/cyberlegion.mjs +1 -1
- package/skills/subagent-backend-governance/SKILL.md +1 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +1 -1
package/.plugin/pins.json
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -6139,6 +6139,7 @@ function reconcile(ctx, opts) {
|
|
|
6139
6139
|
const cur = storablePane(ctx.env ?? process.env);
|
|
6140
6140
|
if (!cur) return [];
|
|
6141
6141
|
const panes = PANE_ADAPTERS[cur.mux].listPanes(exec);
|
|
6142
|
+
if (panes.length === 0) return [];
|
|
6142
6143
|
const live = new Set(panes.map((p) => p.id));
|
|
6143
6144
|
const changed = [];
|
|
6144
6145
|
for (const rec of listAgents(ctx.store)) {
|
|
@@ -6154,18 +6155,34 @@ function reconcile(ctx, opts) {
|
|
|
6154
6155
|
if (opts?.adopt) changed.push(...adopt(ctx, panes));
|
|
6155
6156
|
return changed;
|
|
6156
6157
|
}
|
|
6157
|
-
/**
|
|
6158
|
-
*
|
|
6159
|
-
*
|
|
6158
|
+
/**
|
|
6159
|
+
* Whether a record's pane is positively gone: its existence probe fails AND its mux answers with a
|
|
6160
|
+
* pane list that does not hold it. `paneExists` alone collapses "unreachable" into "gone", so a mux
|
|
6161
|
+
* that answers no list at all (server down, a failed query) leaves the pane undecided — never gone.
|
|
6162
|
+
* The list is read at most once per mux per call.
|
|
6163
|
+
*/
|
|
6164
|
+
function paneGone(exec, pane, lists) {
|
|
6165
|
+
const adapter = PANE_ADAPTERS[pane.mux];
|
|
6166
|
+
if (adapter.paneExists(exec, { id: pane.id })) return false;
|
|
6167
|
+
let panes = lists.get(pane.mux);
|
|
6168
|
+
if (!panes) {
|
|
6169
|
+
panes = adapter.listPanes(exec);
|
|
6170
|
+
lists.set(pane.mux, panes);
|
|
6171
|
+
}
|
|
6172
|
+
return panes.length > 0 && !panes.some((p) => p.id === pane.id);
|
|
6173
|
+
}
|
|
6174
|
+
/** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
|
|
6175
|
+
* first, then judges each remaining record (covers the other mux and sessions outside any
|
|
6176
|
+
* multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
|
|
6177
|
+
* whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
|
|
6160
6178
|
function prune(ctx) {
|
|
6161
6179
|
const exec = ctx.exec ?? realExec;
|
|
6162
6180
|
const now = ctx.now?.() ?? Date.now();
|
|
6163
6181
|
const changed = reconcile(ctx);
|
|
6182
|
+
const lists = /* @__PURE__ */ new Map();
|
|
6164
6183
|
for (const rec of listAgents(ctx.store)) {
|
|
6165
6184
|
if (!isPrunable(rec)) continue;
|
|
6166
|
-
|
|
6167
|
-
const stale = now - new Date(rec.lastSeen).getTime() > STALE_MS;
|
|
6168
|
-
if (paneGone || stale) {
|
|
6185
|
+
if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
|
|
6169
6186
|
rec.status = "exited";
|
|
6170
6187
|
saveAgent(ctx.store, rec);
|
|
6171
6188
|
changed.push(rec);
|
|
@@ -6880,7 +6897,10 @@ function spawn(ctx, input) {
|
|
|
6880
6897
|
assertDistinctFromPrimary(opened.worktree.root, primaryRoot);
|
|
6881
6898
|
ensureMarker(join(opened.worktree.root, ".agents", "cyberlegion"));
|
|
6882
6899
|
cwd = opened.worktree.root;
|
|
6883
|
-
worktree =
|
|
6900
|
+
worktree = {
|
|
6901
|
+
...opened.worktree,
|
|
6902
|
+
primaryRoot
|
|
6903
|
+
};
|
|
6884
6904
|
target = opened.target;
|
|
6885
6905
|
} else {
|
|
6886
6906
|
const added = gitWorktreeAdapter.add(exec, {
|
|
@@ -6891,7 +6911,10 @@ function spawn(ctx, input) {
|
|
|
6891
6911
|
assertDistinctFromPrimary(added.root, primaryRoot);
|
|
6892
6912
|
ensureMarker(join(added.root, ".agents", "cyberlegion"));
|
|
6893
6913
|
cwd = added.root;
|
|
6894
|
-
worktree =
|
|
6914
|
+
worktree = {
|
|
6915
|
+
...added,
|
|
6916
|
+
primaryRoot
|
|
6917
|
+
};
|
|
6895
6918
|
target = sessionAdapter.open(exec, {
|
|
6896
6919
|
cwd,
|
|
6897
6920
|
launch: launchLine(),
|
|
@@ -7584,12 +7607,18 @@ function decommission(ctx, input) {
|
|
|
7584
7607
|
const exec = ctx.exec ?? realExec;
|
|
7585
7608
|
const env = ctx.env ?? process.env;
|
|
7586
7609
|
const worktreeRoot = rec.worktree?.root;
|
|
7587
|
-
const primaryRoot = worktreeRoot ? resolvePrimaryRoot(exec) : void 0;
|
|
7588
|
-
if (worktreeRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
|
|
7589
7610
|
const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
|
|
7611
|
+
const primaryRoot = worktreeExists ? primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
|
|
7612
|
+
if (worktreeRoot && primaryRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
|
|
7590
7613
|
const removesWorktree = worktreeExists && !input.keepWorktree;
|
|
7591
7614
|
if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
|
|
7592
7615
|
const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
|
|
7616
|
+
if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
7617
|
+
"-C",
|
|
7618
|
+
primaryRoot,
|
|
7619
|
+
"worktree",
|
|
7620
|
+
"prune"
|
|
7621
|
+
]);
|
|
7593
7622
|
if (removesWorktree) try {
|
|
7594
7623
|
gitWorktreeAdapter.remove(exec, worktreeRoot, { primaryRoot });
|
|
7595
7624
|
} catch (err) {
|
|
@@ -7610,6 +7639,22 @@ function decommission(ctx, input) {
|
|
|
7610
7639
|
};
|
|
7611
7640
|
}
|
|
7612
7641
|
/**
|
|
7642
|
+
* The primary checkout of the repository `worktreeRoot` belongs to — `--git-common-dir` asked of the
|
|
7643
|
+
* worktree itself, so the answer does not depend on where the caller runs. Throws for a directory git
|
|
7644
|
+
* does not know: there is no repository to remove it from.
|
|
7645
|
+
*/
|
|
7646
|
+
function primaryRootOf(exec, worktreeRoot) {
|
|
7647
|
+
const commonDir = exec("git", [
|
|
7648
|
+
"-C",
|
|
7649
|
+
worktreeRoot,
|
|
7650
|
+
"rev-parse",
|
|
7651
|
+
"--path-format=absolute",
|
|
7652
|
+
"--git-common-dir"
|
|
7653
|
+
]);
|
|
7654
|
+
if (!commonDir) throw new Error(`cannot resolve the repository of worktree ${worktreeRoot} — git does not know it`);
|
|
7655
|
+
return dirname(commonDir);
|
|
7656
|
+
}
|
|
7657
|
+
/**
|
|
7613
7658
|
* Whether the worktree holds work a removal would discard. The marker `spawn` stamps into every
|
|
7614
7659
|
* worktree it creates is cyberlegion's own file, not the unit's work: in a project that does not
|
|
7615
7660
|
* track it, it shows as untracked in every spawned worktree, and counting it would make a plain close
|
|
@@ -7644,10 +7689,7 @@ const VENDORS = {
|
|
|
7644
7689
|
claude: {
|
|
7645
7690
|
file: ".claude/settings.json",
|
|
7646
7691
|
shape: "claude",
|
|
7647
|
-
events: {
|
|
7648
|
-
SessionStart: "SessionStart",
|
|
7649
|
-
PostToolUse: "PostToolUse"
|
|
7650
|
-
},
|
|
7692
|
+
events: { SessionStart: "SessionStart" },
|
|
7651
7693
|
pluginHook: true
|
|
7652
7694
|
},
|
|
7653
7695
|
cursor: {
|
|
@@ -7659,13 +7701,11 @@ const VENDORS = {
|
|
|
7659
7701
|
codex: {
|
|
7660
7702
|
file: ".codex/hooks.json",
|
|
7661
7703
|
shape: "cursor",
|
|
7662
|
-
events: {
|
|
7663
|
-
SessionStart: "SessionStart",
|
|
7664
|
-
PostToolUse: "PostToolUse"
|
|
7665
|
-
},
|
|
7704
|
+
events: { SessionStart: "SessionStart" },
|
|
7666
7705
|
pluginHook: true
|
|
7667
7706
|
}
|
|
7668
7707
|
};
|
|
7708
|
+
const RETIRED_EVENTS = ["PostToolUse"];
|
|
7669
7709
|
function readJson(file) {
|
|
7670
7710
|
if (!existsSync(file)) return {};
|
|
7671
7711
|
try {
|
|
@@ -7709,6 +7749,17 @@ function install(harness, projectDir = process.cwd(), pin) {
|
|
|
7709
7749
|
status
|
|
7710
7750
|
});
|
|
7711
7751
|
}
|
|
7752
|
+
if (spec.pluginHook) for (const event of RETIRED_EVENTS) {
|
|
7753
|
+
if (!(spec.shape === "claude" ? removeClaude(settings, event, event) : removeCursor(settings, event, event))) continue;
|
|
7754
|
+
changed = true;
|
|
7755
|
+
results.push({
|
|
7756
|
+
harness,
|
|
7757
|
+
event,
|
|
7758
|
+
vendorEvent: event,
|
|
7759
|
+
file,
|
|
7760
|
+
status: "removed project hook"
|
|
7761
|
+
});
|
|
7762
|
+
}
|
|
7712
7763
|
if (changed) writeJson$1(file, settings);
|
|
7713
7764
|
return results;
|
|
7714
7765
|
}
|
|
@@ -7723,12 +7774,10 @@ function upsertClaude(settings, event, command) {
|
|
|
7723
7774
|
return "already present";
|
|
7724
7775
|
}
|
|
7725
7776
|
}
|
|
7726
|
-
|
|
7777
|
+
groups.push({ hooks: [{
|
|
7727
7778
|
type: "command",
|
|
7728
7779
|
command
|
|
7729
|
-
}] };
|
|
7730
|
-
if (event === "PostToolUse") group.matcher = "Write|Edit";
|
|
7731
|
-
groups.push(group);
|
|
7780
|
+
}] });
|
|
7732
7781
|
return "registered";
|
|
7733
7782
|
}
|
|
7734
7783
|
function upsertCursor(settings, event, command) {
|
|
@@ -7932,7 +7981,7 @@ function byPath(ctx, dir) {
|
|
|
7932
7981
|
}
|
|
7933
7982
|
//#endregion
|
|
7934
7983
|
//#region src/runtime/inject-inbox.ts
|
|
7935
|
-
const EVENTS = ["SessionStart"
|
|
7984
|
+
const EVENTS = ["SessionStart"];
|
|
7936
7985
|
/**
|
|
7937
7986
|
* Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
|
|
7938
7987
|
* the hub's main pane), and return the SessionStart-style injection payload — or null when there is
|
|
@@ -7980,9 +8029,6 @@ function injectInbox(ctx, event) {
|
|
|
7980
8029
|
}
|
|
7981
8030
|
}
|
|
7982
8031
|
} catch {}
|
|
7983
|
-
if (rec && !rec.spawnedBy) try {
|
|
7984
|
-
if (cur ? !ctx.store.getMainPane() : !listAgents(ctx.store).some((a) => a.kind === "standing")) parts.push("## Legion setup\n\nThis pane has no owner inbox bound yet — run `cyberlegion init` to register the surfacing hook and bind this pane as the owner live presence.");
|
|
7985
|
-
} catch {}
|
|
7986
8032
|
if (parts.length === 0) return null;
|
|
7987
8033
|
return { hookSpecificOutput: {
|
|
7988
8034
|
hookEventName: event,
|
|
@@ -9792,7 +9838,7 @@ withGlobals(mail.command("watch")).description("stream new matching mail as it a
|
|
|
9792
9838
|
});
|
|
9793
9839
|
});
|
|
9794
9840
|
});
|
|
9795
|
-
withGlobals(mail.command("hook")).description("emit the harness hook injection payload (raw JSON on stdout, not TOON)").option("--event <event>", "SessionStart
|
|
9841
|
+
withGlobals(mail.command("hook")).description("emit the harness hook injection payload (raw JSON on stdout, not TOON)").option("--event <event>", "SessionStart", "SessionStart").action((opts) => {
|
|
9796
9842
|
const ctx = ctxOf(opts);
|
|
9797
9843
|
touch(ctx);
|
|
9798
9844
|
const payload = injectInbox(ctx, opts.event);
|
package/dist/index.d.mts
CHANGED
|
@@ -25,9 +25,13 @@ interface AgentRecord {
|
|
|
25
25
|
/** Absent for a standing record (a human/owner principal — no claude/cursor/codex harness). */
|
|
26
26
|
harness?: Harness;
|
|
27
27
|
cwd: string;
|
|
28
|
+
/** `primaryRoot` is the primary checkout of the repository the worktree belongs to, recorded by
|
|
29
|
+
* spawn — the one place to find git's registration of a worktree whose directory is gone. Absent
|
|
30
|
+
* on a registered session and on records from before it was recorded. */
|
|
28
31
|
worktree?: {
|
|
29
32
|
root: string;
|
|
30
33
|
branch?: string;
|
|
34
|
+
primaryRoot?: string;
|
|
31
35
|
} | null;
|
|
32
36
|
/** Where this session lives, tagged with its multiplexer so `prune` runs the right liveness check.
|
|
33
37
|
* `null` for a session in no pane and for a standing record. `window`/`session` are tmux-only. */
|
|
@@ -220,9 +224,10 @@ export declare function resolveAgent(store: Store, ref: string): AgentRecord;
|
|
|
220
224
|
export declare function bumpLastSeen(ctx: IdContext, id: string): void;
|
|
221
225
|
/** Refresh this session's own last-seen if it is registered — best-effort, never throws. */
|
|
222
226
|
export declare function touch(ctx: IdContext): void;
|
|
223
|
-
/** Mark agents whose
|
|
224
|
-
*
|
|
225
|
-
*
|
|
227
|
+
/** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
|
|
228
|
+
* first, then judges each remaining record (covers the other mux and sessions outside any
|
|
229
|
+
* multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
|
|
230
|
+
* whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
|
|
226
231
|
export declare function prune(ctx: IdContext): AgentRecord[];
|
|
227
232
|
//#endregion
|
|
228
233
|
//#region src/agentdef/resolve.d.ts
|
|
@@ -328,7 +333,7 @@ interface DecommissionResult {
|
|
|
328
333
|
export declare function decommission(ctx: IdContext, input: DecommissionInput): DecommissionResult;
|
|
329
334
|
//#endregion
|
|
330
335
|
//#region src/runtime/inject-inbox.d.ts
|
|
331
|
-
type HookEvent = 'SessionStart'
|
|
336
|
+
type HookEvent = 'SessionStart';
|
|
332
337
|
interface InjectPayload {
|
|
333
338
|
hookSpecificOutput: {
|
|
334
339
|
hookEventName: HookEvent;
|
|
@@ -347,10 +352,11 @@ interface InjectPayload {
|
|
|
347
352
|
export declare function injectInbox(ctx: IdContext, event: string): InjectPayload | null;
|
|
348
353
|
//#endregion
|
|
349
354
|
//#region src/install.d.ts
|
|
355
|
+
type RetiredEvent = 'PostToolUse';
|
|
350
356
|
type InstallStatus = 'registered' | 'already present' | 'provided by plugin' | 'removed project hook';
|
|
351
357
|
interface InstallResult {
|
|
352
358
|
harness: Harness;
|
|
353
|
-
event: HookEvent;
|
|
359
|
+
event: HookEvent | RetiredEvent;
|
|
354
360
|
vendorEvent: string;
|
|
355
361
|
file: string;
|
|
356
362
|
status: InstallStatus;
|
package/dist/index.mjs
CHANGED
|
@@ -462,6 +462,7 @@ function reconcile(ctx, opts) {
|
|
|
462
462
|
const cur = storablePane(ctx.env ?? process.env);
|
|
463
463
|
if (!cur) return [];
|
|
464
464
|
const panes = PANE_ADAPTERS[cur.mux].listPanes(exec);
|
|
465
|
+
if (panes.length === 0) return [];
|
|
465
466
|
const live = new Set(panes.map((p) => p.id));
|
|
466
467
|
const changed = [];
|
|
467
468
|
for (const rec of listAgents(ctx.store)) {
|
|
@@ -477,18 +478,34 @@ function reconcile(ctx, opts) {
|
|
|
477
478
|
if (opts?.adopt) changed.push(...adopt(ctx, panes));
|
|
478
479
|
return changed;
|
|
479
480
|
}
|
|
480
|
-
/**
|
|
481
|
-
*
|
|
482
|
-
*
|
|
481
|
+
/**
|
|
482
|
+
* Whether a record's pane is positively gone: its existence probe fails AND its mux answers with a
|
|
483
|
+
* pane list that does not hold it. `paneExists` alone collapses "unreachable" into "gone", so a mux
|
|
484
|
+
* that answers no list at all (server down, a failed query) leaves the pane undecided — never gone.
|
|
485
|
+
* The list is read at most once per mux per call.
|
|
486
|
+
*/
|
|
487
|
+
function paneGone(exec, pane, lists) {
|
|
488
|
+
const adapter = PANE_ADAPTERS[pane.mux];
|
|
489
|
+
if (adapter.paneExists(exec, { id: pane.id })) return false;
|
|
490
|
+
let panes = lists.get(pane.mux);
|
|
491
|
+
if (!panes) {
|
|
492
|
+
panes = adapter.listPanes(exec);
|
|
493
|
+
lists.set(pane.mux, panes);
|
|
494
|
+
}
|
|
495
|
+
return panes.length > 0 && !panes.some((p) => p.id === pane.id);
|
|
496
|
+
}
|
|
497
|
+
/** Mark agents whose session is gone as exited. Reconcile-culls against the current mux's live set
|
|
498
|
+
* first, then judges each remaining record (covers the other mux and sessions outside any
|
|
499
|
+
* multiplexer pane): a pane-bound record by its pane alone — a live or unprobeable pane keeps it
|
|
500
|
+
* whatever its last-seen — and a pane-less record by the staleness timer, its only signal. */
|
|
483
501
|
function prune(ctx) {
|
|
484
502
|
const exec = ctx.exec ?? realExec;
|
|
485
503
|
const now = ctx.now?.() ?? Date.now();
|
|
486
504
|
const changed = reconcile(ctx);
|
|
505
|
+
const lists = /* @__PURE__ */ new Map();
|
|
487
506
|
for (const rec of listAgents(ctx.store)) {
|
|
488
507
|
if (!isPrunable(rec)) continue;
|
|
489
|
-
|
|
490
|
-
const stale = now - new Date(rec.lastSeen).getTime() > STALE_MS;
|
|
491
|
-
if (paneGone || stale) {
|
|
508
|
+
if (rec.pane ? paneGone(exec, rec.pane, lists) : now - new Date(rec.lastSeen).getTime() > STALE_MS) {
|
|
492
509
|
rec.status = "exited";
|
|
493
510
|
saveAgent(ctx.store, rec);
|
|
494
511
|
changed.push(rec);
|
|
@@ -1099,7 +1116,10 @@ function spawn(ctx, input) {
|
|
|
1099
1116
|
assertDistinctFromPrimary$1(opened.worktree.root, primaryRoot);
|
|
1100
1117
|
ensureMarker(join(opened.worktree.root, ".agents", "cyberlegion"));
|
|
1101
1118
|
cwd = opened.worktree.root;
|
|
1102
|
-
worktree =
|
|
1119
|
+
worktree = {
|
|
1120
|
+
...opened.worktree,
|
|
1121
|
+
primaryRoot
|
|
1122
|
+
};
|
|
1103
1123
|
target = opened.target;
|
|
1104
1124
|
} else {
|
|
1105
1125
|
const added = gitWorktreeAdapter$1.add(exec, {
|
|
@@ -1110,7 +1130,10 @@ function spawn(ctx, input) {
|
|
|
1110
1130
|
assertDistinctFromPrimary$1(added.root, primaryRoot);
|
|
1111
1131
|
ensureMarker(join(added.root, ".agents", "cyberlegion"));
|
|
1112
1132
|
cwd = added.root;
|
|
1113
|
-
worktree =
|
|
1133
|
+
worktree = {
|
|
1134
|
+
...added,
|
|
1135
|
+
primaryRoot
|
|
1136
|
+
};
|
|
1114
1137
|
target = sessionAdapter.open(exec, {
|
|
1115
1138
|
cwd,
|
|
1116
1139
|
launch: launchLine(),
|
|
@@ -1577,12 +1600,18 @@ function decommission(ctx, input) {
|
|
|
1577
1600
|
const exec = ctx.exec ?? realExec;
|
|
1578
1601
|
const env = ctx.env ?? process.env;
|
|
1579
1602
|
const worktreeRoot = rec.worktree?.root;
|
|
1580
|
-
const primaryRoot = worktreeRoot ? resolvePrimaryRoot$1(exec) : void 0;
|
|
1581
|
-
if (worktreeRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
|
|
1582
1603
|
const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
|
|
1604
|
+
const primaryRoot = worktreeExists ? primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
|
|
1605
|
+
if (worktreeRoot && primaryRoot && resolve(worktreeRoot) === resolve(primaryRoot)) throw new Error(`refusing to decommission "${input.id}" — its worktree is the primary checkout; neither --force nor --keep-worktree overrides this`);
|
|
1583
1606
|
const removesWorktree = worktreeExists && !input.keepWorktree;
|
|
1584
1607
|
if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
|
|
1585
1608
|
const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(input.id);
|
|
1609
|
+
if (worktreeRoot && !worktreeExists && primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
1610
|
+
"-C",
|
|
1611
|
+
primaryRoot,
|
|
1612
|
+
"worktree",
|
|
1613
|
+
"prune"
|
|
1614
|
+
]);
|
|
1586
1615
|
if (removesWorktree) try {
|
|
1587
1616
|
gitWorktreeAdapter$1.remove(exec, worktreeRoot, { primaryRoot });
|
|
1588
1617
|
} catch (err) {
|
|
@@ -1603,6 +1632,22 @@ function decommission(ctx, input) {
|
|
|
1603
1632
|
};
|
|
1604
1633
|
}
|
|
1605
1634
|
/**
|
|
1635
|
+
* The primary checkout of the repository `worktreeRoot` belongs to — `--git-common-dir` asked of the
|
|
1636
|
+
* worktree itself, so the answer does not depend on where the caller runs. Throws for a directory git
|
|
1637
|
+
* does not know: there is no repository to remove it from.
|
|
1638
|
+
*/
|
|
1639
|
+
function primaryRootOf(exec, worktreeRoot) {
|
|
1640
|
+
const commonDir = exec("git", [
|
|
1641
|
+
"-C",
|
|
1642
|
+
worktreeRoot,
|
|
1643
|
+
"rev-parse",
|
|
1644
|
+
"--path-format=absolute",
|
|
1645
|
+
"--git-common-dir"
|
|
1646
|
+
]);
|
|
1647
|
+
if (!commonDir) throw new Error(`cannot resolve the repository of worktree ${worktreeRoot} — git does not know it`);
|
|
1648
|
+
return dirname(commonDir);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1606
1651
|
* Whether the worktree holds work a removal would discard. The marker `spawn` stamps into every
|
|
1607
1652
|
* worktree it creates is cyberlegion's own file, not the unit's work: in a project that does not
|
|
1608
1653
|
* track it, it shows as untracked in every spawned worktree, and counting it would make a plain close
|
|
@@ -1637,10 +1682,7 @@ const VENDORS = {
|
|
|
1637
1682
|
claude: {
|
|
1638
1683
|
file: ".claude/settings.json",
|
|
1639
1684
|
shape: "claude",
|
|
1640
|
-
events: {
|
|
1641
|
-
SessionStart: "SessionStart",
|
|
1642
|
-
PostToolUse: "PostToolUse"
|
|
1643
|
-
},
|
|
1685
|
+
events: { SessionStart: "SessionStart" },
|
|
1644
1686
|
pluginHook: true
|
|
1645
1687
|
},
|
|
1646
1688
|
cursor: {
|
|
@@ -1652,13 +1694,11 @@ const VENDORS = {
|
|
|
1652
1694
|
codex: {
|
|
1653
1695
|
file: ".codex/hooks.json",
|
|
1654
1696
|
shape: "cursor",
|
|
1655
|
-
events: {
|
|
1656
|
-
SessionStart: "SessionStart",
|
|
1657
|
-
PostToolUse: "PostToolUse"
|
|
1658
|
-
},
|
|
1697
|
+
events: { SessionStart: "SessionStart" },
|
|
1659
1698
|
pluginHook: true
|
|
1660
1699
|
}
|
|
1661
1700
|
};
|
|
1701
|
+
const RETIRED_EVENTS = ["PostToolUse"];
|
|
1662
1702
|
function readJson(file) {
|
|
1663
1703
|
if (!existsSync(file)) return {};
|
|
1664
1704
|
try {
|
|
@@ -1702,6 +1742,17 @@ function install(harness, projectDir = process.cwd(), pin) {
|
|
|
1702
1742
|
status
|
|
1703
1743
|
});
|
|
1704
1744
|
}
|
|
1745
|
+
if (spec.pluginHook) for (const event of RETIRED_EVENTS) {
|
|
1746
|
+
if (!(spec.shape === "claude" ? removeClaude(settings, event, event) : removeCursor(settings, event, event))) continue;
|
|
1747
|
+
changed = true;
|
|
1748
|
+
results.push({
|
|
1749
|
+
harness,
|
|
1750
|
+
event,
|
|
1751
|
+
vendorEvent: event,
|
|
1752
|
+
file,
|
|
1753
|
+
status: "removed project hook"
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1705
1756
|
if (changed) writeJson$1(file, settings);
|
|
1706
1757
|
return results;
|
|
1707
1758
|
}
|
|
@@ -1716,12 +1767,10 @@ function upsertClaude(settings, event, command) {
|
|
|
1716
1767
|
return "already present";
|
|
1717
1768
|
}
|
|
1718
1769
|
}
|
|
1719
|
-
|
|
1770
|
+
groups.push({ hooks: [{
|
|
1720
1771
|
type: "command",
|
|
1721
1772
|
command
|
|
1722
|
-
}] };
|
|
1723
|
-
if (event === "PostToolUse") group.matcher = "Write|Edit";
|
|
1724
|
-
groups.push(group);
|
|
1773
|
+
}] });
|
|
1725
1774
|
return "registered";
|
|
1726
1775
|
}
|
|
1727
1776
|
function upsertCursor(settings, event, command) {
|
|
@@ -1925,7 +1974,7 @@ function byPath(ctx, dir) {
|
|
|
1925
1974
|
}
|
|
1926
1975
|
//#endregion
|
|
1927
1976
|
//#region src/runtime/inject-inbox.ts
|
|
1928
|
-
const EVENTS = ["SessionStart"
|
|
1977
|
+
const EVENTS = ["SessionStart"];
|
|
1929
1978
|
/**
|
|
1930
1979
|
* Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
|
|
1931
1980
|
* the hub's main pane), and return the SessionStart-style injection payload — or null when there is
|
|
@@ -1973,9 +2022,6 @@ function injectInbox(ctx, event) {
|
|
|
1973
2022
|
}
|
|
1974
2023
|
}
|
|
1975
2024
|
} catch {}
|
|
1976
|
-
if (rec && !rec.spawnedBy) try {
|
|
1977
|
-
if (cur ? !ctx.store.getMainPane() : !listAgents(ctx.store).some((a) => a.kind === "standing")) parts.push("## Legion setup\n\nThis pane has no owner inbox bound yet — run `cyberlegion init` to register the surfacing hook and bind this pane as the owner live presence.");
|
|
1978
|
-
} catch {}
|
|
1979
2025
|
if (parts.length === 0) return null;
|
|
1980
2026
|
return { hookSpecificOutput: {
|
|
1981
2027
|
hookEventName: event,
|
package/hooks/hooks.json
CHANGED
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": "1.
|
|
4
|
+
"version": "1.2.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.2.0`.
|
|
21
21
|
|
|
22
22
|
## 1. Resolve the agent definition
|
|
23
23
|
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|
package/skills/legate/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ logic, loads no other governance, and writes no state of its own.
|
|
|
13
13
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
14
14
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
15
15
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
16
|
-
> with the same arguments: `npx -y cyberlegion@1.
|
|
16
|
+
> with the same arguments: `npx -y cyberlegion@1.2.0`.
|
|
17
17
|
|
|
18
18
|
## Doorbell vs mailbox
|
|
19
19
|
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|
|
@@ -13,7 +13,7 @@ the one owner mailbox from wherever they are.
|
|
|
13
13
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
14
14
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
15
15
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
16
|
-
> with the same arguments: `npx -y cyberlegion@1.
|
|
16
|
+
> with the same arguments: `npx -y cyberlegion@1.2.0`.
|
|
17
17
|
|
|
18
18
|
## Resolve the owner handle
|
|
19
19
|
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|
|
@@ -15,7 +15,7 @@ agent (`headless-legate`, `sdd-automaton`, the cold judges) to know how *it* rep
|
|
|
15
15
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
16
16
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
17
17
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
18
|
-
> with the same arguments: `npx -y cyberlegion@1.
|
|
18
|
+
> with the same arguments: `npx -y cyberlegion@1.2.0`.
|
|
19
19
|
|
|
20
20
|
## The one probe: who, if anyone, collects my return?
|
|
21
21
|
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|
|
@@ -14,7 +14,7 @@ the unit's **role** — see *Mid-turn messaging is scoped by role*, below.
|
|
|
14
14
|
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
15
15
|
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
16
16
|
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
17
|
-
> with the same arguments: `npx -y cyberlegion@1.
|
|
17
|
+
> with the same arguments: `npx -y cyberlegion@1.2.0`.
|
|
18
18
|
|
|
19
19
|
## 1. Resolve the agent def
|
|
20
20
|
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
|
-
const FALLBACK = 'npx -y cyberlegion@1.
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@1.2.0'
|
|
15
15
|
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
16
|
|
|
17
17
|
if (existsSync(fileURLToPath(bin))) {
|