cyberlegion 1.10.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 +28 -0
- package/dist/cli.mjs +400 -145
- package/dist/index.d.mts +50 -49
- package/dist/index.mjs +335 -77
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/readme.md +1 -1
- package/skills/dispatch-governance/SKILL.md +16 -3
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +50 -50
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +50 -50
- package/skills/legate/SKILL.md +1 -1
- package/skills/legate/scripts/cyberlegion.mjs +50 -50
- package/skills/manage-inbox/SKILL.md +1 -1
- package/skills/manage-inbox/scripts/cyberlegion.mjs +50 -50
- package/skills/relay-governance/SKILL.md +1 -1
- package/skills/relay-governance/scripts/cyberlegion.mjs +50 -50
- package/skills/subagent-backend-governance/SKILL.md +1 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +50 -50
package/dist/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
10
|
import { lstat, readFile, readdir, readlink, realpath, unlink, writeFile } from "node:fs/promises";
|
|
11
|
-
|
|
11
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/error.js
|
|
12
12
|
/**
|
|
13
13
|
* CommanderError class
|
|
14
14
|
*/
|
|
@@ -42,8 +42,7 @@ var InvalidArgumentError = class extends CommanderError {
|
|
|
42
42
|
this.name = this.constructor.name;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
-
|
|
46
|
-
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/argument.js
|
|
45
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/argument.js
|
|
47
46
|
var Argument = class {
|
|
48
47
|
/**
|
|
49
48
|
* Initialize a new command argument with the given name and description.
|
|
@@ -161,8 +160,7 @@ function humanReadableArgName(arg) {
|
|
|
161
160
|
const nameOutput = arg.name() + (arg.variadic === true ? "..." : "");
|
|
162
161
|
return arg.required ? "<" + nameOutput + ">" : "[" + nameOutput + "]";
|
|
163
162
|
}
|
|
164
|
-
|
|
165
|
-
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/help.js
|
|
163
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/help.js
|
|
166
164
|
/**
|
|
167
165
|
* TypeScript import types for JSDoc, used by Visual Studio Code IntelliSense and `npm run typescript-checkJS`
|
|
168
166
|
* https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types
|
|
@@ -230,8 +228,8 @@ var Help = class {
|
|
|
230
228
|
const removeShort = helpOption.short && cmd._findOption(helpOption.short);
|
|
231
229
|
const removeLong = helpOption.long && cmd._findOption(helpOption.long);
|
|
232
230
|
if (!removeShort && !removeLong) visibleOptions.push(helpOption);
|
|
233
|
-
else if (helpOption.long && !removeLong) visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description));
|
|
234
|
-
else if (helpOption.short && !removeShort) visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description));
|
|
231
|
+
else if (helpOption.long && !removeLong) visibleOptions.push(cmd.createOption(helpOption.long, helpOption.description).helpGroup(helpOption.helpGroupHeading));
|
|
232
|
+
else if (helpOption.short && !removeShort) visibleOptions.push(cmd.createOption(helpOption.short, helpOption.description).helpGroup(helpOption.helpGroupHeading));
|
|
235
233
|
}
|
|
236
234
|
if (this.sortOptions) visibleOptions.sort(this.compareOptions);
|
|
237
235
|
return visibleOptions;
|
|
@@ -636,8 +634,7 @@ var Help = class {
|
|
|
636
634
|
return wrappedLines.join("\n");
|
|
637
635
|
}
|
|
638
636
|
};
|
|
639
|
-
|
|
640
|
-
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/option.js
|
|
637
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/option.js
|
|
641
638
|
var Option = class {
|
|
642
639
|
/**
|
|
643
640
|
* Initialize a new `Option` with the given `flags` and `description`.
|
|
@@ -650,7 +647,7 @@ var Option = class {
|
|
|
650
647
|
this.description = description || "";
|
|
651
648
|
this.required = flags.includes("<");
|
|
652
649
|
this.optional = flags.includes("[");
|
|
653
|
-
this.variadic = /\
|
|
650
|
+
this.variadic = /\p{ID_Continue}\.\.\.[>\]]$/u.test(flags);
|
|
654
651
|
this.mandatory = false;
|
|
655
652
|
const optionFlags = splitOptionFlags(flags);
|
|
656
653
|
this.short = optionFlags.shortFlag;
|
|
@@ -934,8 +931,7 @@ function splitOptionFlags(flags) {
|
|
|
934
931
|
longFlag
|
|
935
932
|
};
|
|
936
933
|
}
|
|
937
|
-
|
|
938
|
-
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/suggestSimilar.js
|
|
934
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/suggestSimilar.js
|
|
939
935
|
const maxDistance = 3;
|
|
940
936
|
function editDistance(a, b) {
|
|
941
937
|
if (Math.abs(a.length - b.length) > maxDistance) return Math.max(a.length, b.length);
|
|
@@ -986,8 +982,7 @@ function suggestSimilar(word, candidates) {
|
|
|
986
982
|
if (similar.length === 1) return `\n(Did you mean ${similar[0]}?)`;
|
|
987
983
|
return "";
|
|
988
984
|
}
|
|
989
|
-
|
|
990
|
-
//#region ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/lib/command.js
|
|
985
|
+
// ../../node_modules/.pnpm/commander@15.1.0/node_modules/commander/lib/command.js
|
|
991
986
|
var Command = class Command extends EventEmitter {
|
|
992
987
|
/**
|
|
993
988
|
* Initialize a new `Command`.
|
|
@@ -1737,7 +1732,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1737
1732
|
getOptionValueSourceWithGlobals(key) {
|
|
1738
1733
|
let source;
|
|
1739
1734
|
this._getCommandAndAncestors().forEach((cmd) => {
|
|
1740
|
-
if (cmd.
|
|
1735
|
+
if (Object.hasOwn(cmd._optionValueSources, key)) source = cmd.getOptionValueSource(key);
|
|
1741
1736
|
});
|
|
1742
1737
|
return source;
|
|
1743
1738
|
}
|
|
@@ -2246,7 +2241,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2246
2241
|
return arg.length > 1 && arg[0] === "-";
|
|
2247
2242
|
}
|
|
2248
2243
|
const negativeNumberArg = (arg) => {
|
|
2249
|
-
if (!/^-(\d+|\d*\.\d+)(
|
|
2244
|
+
if (!/^-(\d+|\d*\.\d+)([eE][+-]?\d+)?$/.test(arg)) return false;
|
|
2250
2245
|
return !this._getCommandAndAncestors().some((cmd) => cmd.options.map((opt) => opt.short).some((short) => /^-\d$/.test(short)));
|
|
2251
2246
|
};
|
|
2252
2247
|
let activeVariadicOption = null;
|
|
@@ -2646,7 +2641,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2646
2641
|
* (This does not override a group set directly on the subcommand using .helpGroup().)
|
|
2647
2642
|
*
|
|
2648
2643
|
* @example
|
|
2649
|
-
* program.commandsGroup('Development Commands:);
|
|
2644
|
+
* program.commandsGroup('Development Commands:');
|
|
2650
2645
|
* program.command('watch')...
|
|
2651
2646
|
* program.command('lint')...
|
|
2652
2647
|
* ...
|
|
@@ -2815,7 +2810,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2815
2810
|
* Pass in false to disable the built-in help option.
|
|
2816
2811
|
*
|
|
2817
2812
|
* @example
|
|
2818
|
-
* program.helpOption('-?, --help' 'show help'); // customise
|
|
2813
|
+
* program.helpOption('-?, --help', 'show help'); // customise
|
|
2819
2814
|
* program.helpOption(false); // disable
|
|
2820
2815
|
*
|
|
2821
2816
|
* @param {(string | boolean)} flags
|
|
@@ -2937,12 +2932,12 @@ function incrementNodeInspectorPort(args) {
|
|
|
2937
2932
|
let debugHost = "127.0.0.1";
|
|
2938
2933
|
let debugPort = "9229";
|
|
2939
2934
|
let match;
|
|
2940
|
-
if ((match = arg.match(/^(--inspect(-brk)?)$/)) !== null) debugOption = match[1];
|
|
2941
|
-
else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+)$/)) !== null) {
|
|
2935
|
+
if ((match = arg.match(/^(--inspect(-brk|-wait)?)$/)) !== null) debugOption = match[1];
|
|
2936
|
+
else if ((match = arg.match(/^(--inspect(-brk|-port|-wait)?)=(\[[^\]]+\]|[^:]+)$/)) !== null) {
|
|
2942
2937
|
debugOption = match[1];
|
|
2943
2938
|
if (/^\d+$/.test(match[3])) debugPort = match[3];
|
|
2944
2939
|
else debugHost = match[3];
|
|
2945
|
-
} else if ((match = arg.match(/^(--inspect(-brk|-port)?)=([^:]+):(\d+)$/)) !== null) {
|
|
2940
|
+
} else if ((match = arg.match(/^(--inspect(-brk|-port|-wait)?)=(\[[^\]]+\]|[^:]+):(\d+)$/)) !== null) {
|
|
2946
2941
|
debugOption = match[1];
|
|
2947
2942
|
debugHost = match[3];
|
|
2948
2943
|
debugPort = match[4];
|
|
@@ -2962,8 +2957,7 @@ function useColor() {
|
|
|
2962
2957
|
if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
|
|
2963
2958
|
}
|
|
2964
2959
|
new Command();
|
|
2965
|
-
|
|
2966
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/exec-Bmdcg_T7.mjs
|
|
2960
|
+
// ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/exec-Bmdcg_T7.mjs
|
|
2967
2961
|
const nodeExec$1 = (cmd, args) => {
|
|
2968
2962
|
try {
|
|
2969
2963
|
const out = execFileSync(cmd, args, {
|
|
@@ -2990,8 +2984,7 @@ const nodeExec$1 = (cmd, args) => {
|
|
|
2990
2984
|
function withReason(exec, message) {
|
|
2991
2985
|
return exec.lastError ? `${message} — ${exec.lastError}` : message;
|
|
2992
2986
|
}
|
|
2993
|
-
|
|
2994
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/backend-D3VSsev4.mjs
|
|
2987
|
+
// ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/backend-D3VSsev4.mjs
|
|
2995
2988
|
const KNOWN_MUX = [
|
|
2996
2989
|
"tmux",
|
|
2997
2990
|
"rmux",
|
|
@@ -9138,8 +9131,7 @@ function callerPane(adapter, env) {
|
|
|
9138
9131
|
const self = currentPane(env);
|
|
9139
9132
|
return self && self.mux === adapter.name ? { id: self.pane } : void 0;
|
|
9140
9133
|
}
|
|
9141
|
-
|
|
9142
|
-
//#region src/admin.ts
|
|
9134
|
+
// src/admin.ts
|
|
9143
9135
|
/**
|
|
9144
9136
|
* Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
|
|
9145
9137
|
* the destination). Used by `admin migrate` when moving state between an old project-local root
|
|
@@ -9173,8 +9165,7 @@ function migrateStore(from, to) {
|
|
|
9173
9165
|
briefs
|
|
9174
9166
|
};
|
|
9175
9167
|
}
|
|
9176
|
-
|
|
9177
|
-
//#region ../../node_modules/.pnpm/cyber-agentkit@2.0.0/node_modules/cyber-agentkit/dist/worktrees.js
|
|
9168
|
+
// ../../node_modules/.pnpm/cyber-agentkit@2.2.0/node_modules/cyber-agentkit/dist/worktrees.js
|
|
9178
9169
|
/**
|
|
9179
9170
|
* Whether a checkout has uncommitted changes, tracked or untracked, or `undefined` when git could not
|
|
9180
9171
|
* say. `undefined` is not `false`: a caller deciding whether a worktree is safe to reuse must not read
|
|
@@ -10247,8 +10238,7 @@ async function exists(path) {
|
|
|
10247
10238
|
return false;
|
|
10248
10239
|
}
|
|
10249
10240
|
}
|
|
10250
|
-
|
|
10251
|
-
//#region ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/worktree.mjs
|
|
10241
|
+
// ../../node_modules/.pnpm/cyber-mux@0.12.0/node_modules/cyber-mux/dist/worktree.mjs
|
|
10252
10242
|
/**
|
|
10253
10243
|
* This module's own refusals and failures — plain cyber-mux prose, never a dependency's raw words.
|
|
10254
10244
|
* `reportWorktreeFailure` (`cli.ts`) forwards a `WorktreeGitError`'s message onto stdout verbatim
|
|
@@ -10309,8 +10299,7 @@ function resolvePrimaryRoot(exec) {
|
|
|
10309
10299
|
function assertDistinctFromPrimary(worktreeRoot, primaryRoot) {
|
|
10310
10300
|
if (resolve(worktreeRoot) === resolve(primaryRoot)) throw new WorktreeGitError("refusing to run in the primary checkout — spawn a worktree distinct from the primary checkout");
|
|
10311
10301
|
}
|
|
10312
|
-
|
|
10313
|
-
//#region src/paths.ts
|
|
10302
|
+
// src/paths.ts
|
|
10314
10303
|
/** The tracked marker file that makes a hub root initialized (see ensureMarker). */
|
|
10315
10304
|
const MARKER_FILE = "config.json";
|
|
10316
10305
|
/** Where `spawn` stamps that marker inside a worktree it creates, relative to the worktree root. */
|
|
@@ -10406,8 +10395,7 @@ const paths = {
|
|
|
10406
10395
|
function sanitizePane(pane) {
|
|
10407
10396
|
return pane.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
10408
10397
|
}
|
|
10409
|
-
|
|
10410
|
-
//#region src/mux-env.ts
|
|
10398
|
+
// src/mux-env.ts
|
|
10411
10399
|
/**
|
|
10412
10400
|
* Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
|
|
10413
10401
|
* is still honored"). cyber-mux's own fast-path reads `$CYBER_MUX`/`$CYBER_MUX_PANE` — its
|
|
@@ -10436,8 +10424,7 @@ function normalizeMuxEnv(env) {
|
|
|
10436
10424
|
CYBER_MUX_PANE: env.CYBERLEGION_MUX_PANE
|
|
10437
10425
|
};
|
|
10438
10426
|
}
|
|
10439
|
-
|
|
10440
|
-
//#region src/identity.ts
|
|
10427
|
+
// src/identity.ts
|
|
10441
10428
|
const realExec = nodeExec$1;
|
|
10442
10429
|
/** `currentPane`, narrowed to a backend the registry can actually store a locator under. A caller
|
|
10443
10430
|
* inside a pane-carrying-but-unstorable multiplexer (wezterm/zellij) is simply unpaned here — it can
|
|
@@ -10450,6 +10437,133 @@ function storablePane(env) {
|
|
|
10450
10437
|
pane: cur.pane
|
|
10451
10438
|
} : void 0;
|
|
10452
10439
|
}
|
|
10440
|
+
/** The calling session's storable pane, resolved the way `register` resolves it: herdr's answer
|
|
10441
|
+
* when herdr gives a trustworthy one, `$HERDR_PANE_ID` otherwise (#154, #268). */
|
|
10442
|
+
function callingStorablePane(ctx) {
|
|
10443
|
+
return selfPane(storablePane(ctx.env ?? process.env), ctx.exec ?? realExec);
|
|
10444
|
+
}
|
|
10445
|
+
/**
|
|
10446
|
+
* This session's own pane as the multiplexer sees it now. The env names the pane the shell was
|
|
10447
|
+
* launched in, and for herdr that goes stale: herdr gives a pane a new id when it moves to another
|
|
10448
|
+
* tab or workspace, while the shell keeps its launch-time `$HERDR_PANE_ID`, and herdr ids recur on a
|
|
10449
|
+
* hub, so the stale id can be another unit's pane (#154). herdr's `pane current` resolves the
|
|
10450
|
+
* calling pane itself, so its answer wins on a herdr that answers it truly (0.8.2 on). When herdr
|
|
10451
|
+
* gives no answer, or an older herdr disagrees with the env, the env id stands. tmux pane
|
|
10452
|
+
* ids last as long as the pane, so tmux needs no second opinion.
|
|
10453
|
+
*/
|
|
10454
|
+
function selfPane(cur, exec) {
|
|
10455
|
+
if (cur?.mux !== "herdr") return cur;
|
|
10456
|
+
const live = herdrCallingPane(exec);
|
|
10457
|
+
if (!live) return cur;
|
|
10458
|
+
if (live.pane === cur.pane) return {
|
|
10459
|
+
...cur,
|
|
10460
|
+
...live.terminal ? { terminal: live.terminal } : {}
|
|
10461
|
+
};
|
|
10462
|
+
if (!herdrReportsCallingPane(exec)) return cur;
|
|
10463
|
+
return {
|
|
10464
|
+
...cur,
|
|
10465
|
+
pane: live.pane,
|
|
10466
|
+
...live.terminal ? { terminal: live.terminal } : {}
|
|
10467
|
+
};
|
|
10468
|
+
}
|
|
10469
|
+
/** herdr 0.8.2 fixed `pane current` to answer the calling pane; before it, the command answered
|
|
10470
|
+
* another client's focused pane (herdr #2297), which would misattribute exactly as a stale env id
|
|
10471
|
+
* does. A version that cannot be read is treated as too old. */
|
|
10472
|
+
function herdrReportsCallingPane(exec) {
|
|
10473
|
+
const m = exec("herdr", ["--version"])?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
10474
|
+
if (!m) return false;
|
|
10475
|
+
const [major, minor, patch] = m.slice(1).map(Number);
|
|
10476
|
+
return major > 0 || minor > 8 || minor === 8 && patch >= 2;
|
|
10477
|
+
}
|
|
10478
|
+
function herdrCallingPane(exec) {
|
|
10479
|
+
return herdrPaneAnswer(exec("herdr", ["pane", "current"]));
|
|
10480
|
+
}
|
|
10481
|
+
/** Parse a herdr `pane current` / `pane get` answer into its pane id and terminal id. */
|
|
10482
|
+
function herdrPaneAnswer(out) {
|
|
10483
|
+
if (!out) return void 0;
|
|
10484
|
+
try {
|
|
10485
|
+
const pane = JSON.parse(out)?.result?.pane;
|
|
10486
|
+
const id = pane?.pane_id;
|
|
10487
|
+
if (typeof id !== "string" || !id) return void 0;
|
|
10488
|
+
const terminal = pane.terminal_id;
|
|
10489
|
+
return {
|
|
10490
|
+
pane: id,
|
|
10491
|
+
...typeof terminal === "string" && terminal ? { terminal } : {}
|
|
10492
|
+
};
|
|
10493
|
+
} catch {
|
|
10494
|
+
return;
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
/**
|
|
10498
|
+
* The herdr terminal id behind a pane id — what a unit's herdr pane record keys on besides the pane
|
|
10499
|
+
* id. herdr gives a pane a new id when it moves to another workspace and keeps its terminal id
|
|
10500
|
+
* (#272; herdr 0.9.5: `pane move wRD:p2 --tab wRE:t1` answered `wRE:p2` on the same `terminal_id`).
|
|
10501
|
+
* Undefined when herdr does not answer or reports no terminal id.
|
|
10502
|
+
*/
|
|
10503
|
+
function herdrTerminalOf(exec, paneId) {
|
|
10504
|
+
return herdrPaneAnswer(exec("herdr", [
|
|
10505
|
+
"pane",
|
|
10506
|
+
"get",
|
|
10507
|
+
paneId
|
|
10508
|
+
]))?.terminal;
|
|
10509
|
+
}
|
|
10510
|
+
/** The pane locator for a pane a spawn or restart just opened: tagged with its multiplexer so the
|
|
10511
|
+
* unit's own `prune` runs the right liveness check, and for herdr carrying the terminal id a move
|
|
10512
|
+
* keeps. `null` for a backend a record cannot carry. */
|
|
10513
|
+
function openedPaneLocator(exec, mux, id) {
|
|
10514
|
+
if (mux === "tmux") return {
|
|
10515
|
+
mux,
|
|
10516
|
+
id
|
|
10517
|
+
};
|
|
10518
|
+
if (mux !== "herdr") return null;
|
|
10519
|
+
const terminal = herdrTerminalOf(exec, id);
|
|
10520
|
+
return {
|
|
10521
|
+
mux,
|
|
10522
|
+
id,
|
|
10523
|
+
...terminal ? { terminal } : {}
|
|
10524
|
+
};
|
|
10525
|
+
}
|
|
10526
|
+
/** herdr's live panes keyed by terminal id → current pane id. Empty when herdr gives no list, which
|
|
10527
|
+
* says nothing about any pane. */
|
|
10528
|
+
function herdrTerminals(exec) {
|
|
10529
|
+
const map = /* @__PURE__ */ new Map();
|
|
10530
|
+
const out = exec("herdr", ["pane", "list"]);
|
|
10531
|
+
if (!out) return map;
|
|
10532
|
+
try {
|
|
10533
|
+
const panes = JSON.parse(out)?.result?.panes;
|
|
10534
|
+
if (!Array.isArray(panes)) return map;
|
|
10535
|
+
for (const p of panes) if (typeof p?.terminal_id === "string" && typeof p?.pane_id === "string") map.set(p.terminal_id, p.pane_id);
|
|
10536
|
+
} catch {
|
|
10537
|
+
return map;
|
|
10538
|
+
}
|
|
10539
|
+
return map;
|
|
10540
|
+
}
|
|
10541
|
+
/**
|
|
10542
|
+
* Where a herdr record that carries its terminal id lives now: the pane id herdr lists for that
|
|
10543
|
+
* terminal (`{ id }`), `'gone'` when herdr answered with a list that lacks the terminal, or
|
|
10544
|
+
* `undefined` when herdr gave no list. Not called for a record without a terminal id, which keeps
|
|
10545
|
+
* being judged by its pane id.
|
|
10546
|
+
*/
|
|
10547
|
+
function locateTerminal(exec, terminal, cache = {}) {
|
|
10548
|
+
cache.terminals ??= herdrTerminals(exec);
|
|
10549
|
+
if (cache.terminals.size === 0) return void 0;
|
|
10550
|
+
const id = cache.terminals.get(terminal);
|
|
10551
|
+
return id ? { id } : "gone";
|
|
10552
|
+
}
|
|
10553
|
+
/** Point a record whose herdr pane moved at the pane id it has now, moving its pane pointer with it.
|
|
10554
|
+
* The old pointer is dropped only while it still names this record, since herdr hands old ids out
|
|
10555
|
+
* again. */
|
|
10556
|
+
function followMove(ctx, rec, id) {
|
|
10557
|
+
const pane = rec.pane;
|
|
10558
|
+
if (!pane || pane.id === id) return;
|
|
10559
|
+
if (ctx.store.resolvePaneId(pane.id) === rec.id) ctx.store.removePaneIndex(pane.id);
|
|
10560
|
+
rec.pane = {
|
|
10561
|
+
...pane,
|
|
10562
|
+
id
|
|
10563
|
+
};
|
|
10564
|
+
saveAgent(ctx.store, rec);
|
|
10565
|
+
ctx.store.putPaneIndex(id, rec.id);
|
|
10566
|
+
}
|
|
10453
10567
|
const nowIso = (ctx) => new Date(ctx.now?.() ?? Date.now()).toISOString();
|
|
10454
10568
|
function randomId() {
|
|
10455
10569
|
return randomBytes(8).toString("hex");
|
|
@@ -10495,10 +10609,33 @@ function detectHarness(explicit, ctx) {
|
|
|
10495
10609
|
* explicit via the env var.
|
|
10496
10610
|
*/
|
|
10497
10611
|
function resolveSelfId(ctx) {
|
|
10612
|
+
return resolveSelf(ctx).id;
|
|
10613
|
+
}
|
|
10614
|
+
function resolveSelf(ctx) {
|
|
10498
10615
|
const env = ctx.env ?? process.env;
|
|
10499
|
-
const cur = currentPane(normalizeMuxEnv(env));
|
|
10500
|
-
if (cur) return
|
|
10501
|
-
|
|
10616
|
+
const cur = selfPane(currentPane(normalizeMuxEnv(env)), ctx.exec ?? realExec);
|
|
10617
|
+
if (cur) return {
|
|
10618
|
+
id: selfIdAt(ctx.store, cur),
|
|
10619
|
+
cur
|
|
10620
|
+
};
|
|
10621
|
+
return {
|
|
10622
|
+
id: env.CYBERLEGION_AGENT_ID || void 0,
|
|
10623
|
+
cur
|
|
10624
|
+
};
|
|
10625
|
+
}
|
|
10626
|
+
/**
|
|
10627
|
+
* The unit a pane belongs to. The pane pointer answers first. When herdr named the calling pane's
|
|
10628
|
+
* terminal, a pointer whose record carries a different terminal is stale (the id was handed to
|
|
10629
|
+
* this pane after the unit's own pane moved away), and a missing pointer may mean this pane moved:
|
|
10630
|
+
* either way the record bearing this terminal is the caller (#272). A record without a terminal id
|
|
10631
|
+
* is trusted by its pointer as before.
|
|
10632
|
+
*/
|
|
10633
|
+
function selfIdAt(store, cur) {
|
|
10634
|
+
const indexed = store.resolvePaneId(cur.pane);
|
|
10635
|
+
if (!cur.terminal) return indexed;
|
|
10636
|
+
const terminal = indexed ? loadAgent(store, indexed)?.pane?.terminal : void 0;
|
|
10637
|
+
if (indexed && (!terminal || terminal === cur.terminal)) return indexed;
|
|
10638
|
+
return listAgents(store).find((a) => a.pane?.mux === "herdr" && a.pane.terminal === cur.terminal)?.id;
|
|
10502
10639
|
}
|
|
10503
10640
|
/** Register (or idempotently refresh) this session's identity. */
|
|
10504
10641
|
function register(ctx, input) {
|
|
@@ -10510,8 +10647,8 @@ function register(ctx, input) {
|
|
|
10510
10647
|
const existing = existingId ? loadAgent(ctx.store, existingId) : void 0;
|
|
10511
10648
|
const id = existing?.id ?? existingId ?? randomId();
|
|
10512
10649
|
const ts = nowIso(ctx);
|
|
10513
|
-
const cur = storablePane(env);
|
|
10514
10650
|
const exec = ctx.exec ?? realExec;
|
|
10651
|
+
const cur = selfPane(storablePane(env), exec);
|
|
10515
10652
|
const rec = {
|
|
10516
10653
|
id,
|
|
10517
10654
|
handle: input.handle ?? existing?.handle ?? id.slice(0, 6),
|
|
@@ -10546,7 +10683,8 @@ function paneLocator(cur, exec, env) {
|
|
|
10546
10683
|
};
|
|
10547
10684
|
return {
|
|
10548
10685
|
mux: cur.mux,
|
|
10549
|
-
id: cur.pane
|
|
10686
|
+
id: cur.pane,
|
|
10687
|
+
...cur.terminal ? { terminal: cur.terminal } : {}
|
|
10550
10688
|
};
|
|
10551
10689
|
}
|
|
10552
10690
|
/** Derive a standing record's stable id from its handle — same slug rule as `sanitizePane`, prefixed
|
|
@@ -10855,10 +10993,26 @@ function bumpLastSeen(ctx, id) {
|
|
|
10855
10993
|
rec.lastSeen = nowIso(ctx);
|
|
10856
10994
|
saveAgent(ctx.store, rec);
|
|
10857
10995
|
}
|
|
10858
|
-
/**
|
|
10996
|
+
/**
|
|
10997
|
+
* Refresh this session's own last-seen if it is registered — best-effort, never throws. In a herdr
|
|
10998
|
+
* pane whose calling pane herdr reported, it also heals the record's pane to the id and terminal
|
|
10999
|
+
* herdr gave, so a unit whose pane moved fixes its own record on its next turn (#272).
|
|
11000
|
+
*/
|
|
10859
11001
|
function touch(ctx) {
|
|
10860
|
-
const id =
|
|
10861
|
-
if (id)
|
|
11002
|
+
const { id, cur } = resolveSelf(ctx);
|
|
11003
|
+
if (!id) return;
|
|
11004
|
+
bumpLastSeen(ctx, id);
|
|
11005
|
+
if (cur?.mux !== "herdr" || !cur.terminal) return;
|
|
11006
|
+
const rec = loadAgent(ctx.store, id);
|
|
11007
|
+
if (rec?.pane?.mux !== "herdr") return;
|
|
11008
|
+
if (rec.pane.terminal !== cur.terminal) {
|
|
11009
|
+
rec.pane = {
|
|
11010
|
+
...rec.pane,
|
|
11011
|
+
terminal: cur.terminal
|
|
11012
|
+
};
|
|
11013
|
+
saveAgent(ctx.store, rec);
|
|
11014
|
+
}
|
|
11015
|
+
followMove(ctx, rec, cur.pane);
|
|
10862
11016
|
}
|
|
10863
11017
|
const STALE_MS = 9e5;
|
|
10864
11018
|
/** Standing and service records have no session of their own, so nothing about a pane or a timer
|
|
@@ -10885,7 +11039,9 @@ function isPrunable(rec) {
|
|
|
10885
11039
|
function sessionLive(ctx, rec) {
|
|
10886
11040
|
if (rec.status === "exited" || rec.status === "stopped") return false;
|
|
10887
11041
|
if (!rec.pane) return true;
|
|
10888
|
-
const
|
|
11042
|
+
const exec = ctx.exec ?? realExec;
|
|
11043
|
+
if (rec.pane.mux === "herdr" && rec.pane.terminal) return locateTerminal(exec, rec.pane.terminal) !== "gone";
|
|
11044
|
+
const panes = PANE_ADAPTERS[rec.pane.mux].listPanes(exec);
|
|
10889
11045
|
if (panes.length === 0) return true;
|
|
10890
11046
|
const id = rec.pane.id;
|
|
10891
11047
|
return panes.some((p) => p.id === id);
|
|
@@ -10960,11 +11116,12 @@ function reconcile(ctx, opts) {
|
|
|
10960
11116
|
if (panes.length === 0) return [];
|
|
10961
11117
|
const live = new Set(panes.map((p) => p.id));
|
|
10962
11118
|
const changed = [];
|
|
11119
|
+
const cache = {};
|
|
10963
11120
|
for (const rec of listAgents(ctx.store)) {
|
|
10964
11121
|
if (!isPrunable(rec)) continue;
|
|
10965
11122
|
if (!rec.pane) continue;
|
|
10966
11123
|
if (rec.pane.mux !== cur.mux) continue;
|
|
10967
|
-
if (!live.has(rec.pane.id)) {
|
|
11124
|
+
if (rec.pane.mux === "herdr" && rec.pane.terminal ? terminalGone(ctx, rec, rec.pane.terminal, cache) : !live.has(rec.pane.id)) {
|
|
10968
11125
|
rec.status = "exited";
|
|
10969
11126
|
saveAgent(ctx.store, rec);
|
|
10970
11127
|
changed.push(rec);
|
|
@@ -10989,27 +11146,111 @@ function paneGone(exec, pane, lists) {
|
|
|
10989
11146
|
}
|
|
10990
11147
|
return panes.length > 0 && !panes.some((p) => p.id === pane.id);
|
|
10991
11148
|
}
|
|
10992
|
-
/**
|
|
10993
|
-
*
|
|
10994
|
-
*
|
|
10995
|
-
*
|
|
10996
|
-
|
|
11149
|
+
/**
|
|
11150
|
+
* Whether a herdr record that carries its terminal id is positively gone: herdr listed its panes and
|
|
11151
|
+
* that terminal is not among them. A live terminal under a new pane id is a moved pane, so the record
|
|
11152
|
+
* follows it instead (#272). A pane id that herdr now gives another terminal does not keep the record
|
|
11153
|
+
* alive, since herdr ids recur. No list at all decides nothing.
|
|
11154
|
+
*/
|
|
11155
|
+
function terminalGone(ctx, rec, terminal, cache) {
|
|
11156
|
+
const at = locateTerminal(ctx.exec ?? realExec, terminal, cache);
|
|
11157
|
+
if (at === "gone") return true;
|
|
11158
|
+
if (at) followMove(ctx, rec, at.id);
|
|
11159
|
+
return false;
|
|
11160
|
+
}
|
|
11161
|
+
/**
|
|
11162
|
+
* Mark agents whose session is gone as exited, then delete every exited unit with nothing left to
|
|
11163
|
+
* work from. Reconcile-culls against the current mux's live set first, then judges each remaining
|
|
11164
|
+
* record (covers the other mux and sessions outside any multiplexer pane): a pane-bound record by its
|
|
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 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`.
|
|
11171
|
+
*/
|
|
11172
|
+
function prune(ctx, options = {}) {
|
|
10997
11173
|
const exec = ctx.exec ?? realExec;
|
|
10998
11174
|
const now = ctx.now?.() ?? Date.now();
|
|
10999
|
-
const
|
|
11175
|
+
const marked = reconcile(ctx);
|
|
11000
11176
|
const lists = /* @__PURE__ */ new Map();
|
|
11177
|
+
const cache = {};
|
|
11001
11178
|
for (const rec of listAgents(ctx.store)) {
|
|
11002
11179
|
if (!isPrunable(rec)) continue;
|
|
11003
|
-
if (rec.pane ?
|
|
11180
|
+
if (!rec.pane ? now - new Date(rec.lastSeen).getTime() > STALE_MS : rec.pane.mux === "herdr" && rec.pane.terminal ? terminalGone(ctx, rec, rec.pane.terminal, cache) : paneGone(exec, rec.pane, lists)) {
|
|
11004
11181
|
rec.status = "exited";
|
|
11005
11182
|
saveAgent(ctx.store, rec);
|
|
11006
|
-
|
|
11007
|
-
}
|
|
11183
|
+
marked.push(rec);
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
const deleted = deleteExited(ctx, options.landed ?? (() => false));
|
|
11187
|
+
const gone = new Set(deleted.map((r) => r.id));
|
|
11188
|
+
return [...marked.filter((r) => !gone.has(r.id)).map((r) => ({
|
|
11189
|
+
...r,
|
|
11190
|
+
pruned: "exited"
|
|
11191
|
+
})), ...deleted.map((r) => ({
|
|
11192
|
+
...r,
|
|
11193
|
+
pruned: "deleted"
|
|
11194
|
+
}))];
|
|
11195
|
+
}
|
|
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);
|
|
11216
|
+
}
|
|
11217
|
+
/** Whether a worktree root is its repository's primary checkout — the one test `unit close` and
|
|
11218
|
+
* `unit prune` share for "a seat on the main checkout, which nothing removes". */
|
|
11219
|
+
function isPrimaryCheckout(worktreeRoot, primaryRoot) {
|
|
11220
|
+
return worktreeRoot != null && primaryRoot != null && resolve(worktreeRoot) === resolve(primaryRoot);
|
|
11221
|
+
}
|
|
11222
|
+
/**
|
|
11223
|
+
* Delete every exited unit that leaves no work on disk (`keepsWorktree`): its record, brief, mailbox, and the pane
|
|
11224
|
+
* pointer that still names it. Nothing revives an exited unit (`unit restart` takes only a running or
|
|
11225
|
+
* stopped one), so these records are read by nobody. A pane pointer is dropped only when it still
|
|
11226
|
+
* resolves to the deleted unit, since a multiplexer may have handed the pane id to another unit
|
|
11227
|
+
* since. A worktree that is gone from disk may still be registered with git, and the record is the
|
|
11228
|
+
* last place that names its repository, so that registration is cleared first, best-effort, as
|
|
11229
|
+
* `unit close` does. No pane is torn down: an exited unit's pane is already gone or belongs to
|
|
11230
|
+
* someone else.
|
|
11231
|
+
*/
|
|
11232
|
+
function deleteExited(ctx, landed) {
|
|
11233
|
+
const exec = ctx.exec ?? realExec;
|
|
11234
|
+
const deleted = [];
|
|
11235
|
+
for (const rec of listAgents(ctx.store)) {
|
|
11236
|
+
if (rec.status !== "exited" || isSessionIndependent(rec) || keepsWorktree(ctx.store, rec, landed)) continue;
|
|
11237
|
+
const primaryRoot = rec.worktree?.primaryRoot;
|
|
11238
|
+
if (primaryRoot && existsSync(primaryRoot)) exec("git", [
|
|
11239
|
+
"-C",
|
|
11240
|
+
primaryRoot,
|
|
11241
|
+
"worktree",
|
|
11242
|
+
"prune"
|
|
11243
|
+
]);
|
|
11244
|
+
const pane = rec.pane?.id ?? ctx.store.findPaneByAgentId(rec.id);
|
|
11245
|
+
if (pane && ctx.store.resolvePaneId(pane) === rec.id) ctx.store.removePaneIndex(pane);
|
|
11246
|
+
ctx.store.removeAgent(rec.id);
|
|
11247
|
+
ctx.store.removeAgentData(rec.id);
|
|
11248
|
+
ctx.store.removeMailbox(rec.id);
|
|
11249
|
+
deleted.push(rec);
|
|
11008
11250
|
}
|
|
11009
|
-
return
|
|
11251
|
+
return deleted;
|
|
11010
11252
|
}
|
|
11011
|
-
|
|
11012
|
-
//#region src/console/prompt-guard.ts
|
|
11253
|
+
// src/console/prompt-guard.ts
|
|
11013
11254
|
const CODEX_PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
11014
11255
|
"Ask Codex to do anything",
|
|
11015
11256
|
"Explain this codebase",
|
|
@@ -11167,8 +11408,7 @@ async function clearLine(adapter, exec, target, sleep) {
|
|
|
11167
11408
|
adapter.sendText(exec, target, KILL_LINE);
|
|
11168
11409
|
await sleep(KEY_SETTLE_MS);
|
|
11169
11410
|
}
|
|
11170
|
-
|
|
11171
|
-
//#region src/console/ring.ts
|
|
11411
|
+
// src/console/ring.ts
|
|
11172
11412
|
const DEFAULT_ATTEMPTS = 10;
|
|
11173
11413
|
const DEFAULT_SETTLE_MS = 400;
|
|
11174
11414
|
/** cyber-mux's `isStaged` matches on the same prefix length. */
|
|
@@ -11273,8 +11513,7 @@ async function ringTurn(adapter, exec, target, message, opts = {}) {
|
|
|
11273
11513
|
const why = state === "lost" ? "the text vanished without the harness posting it" : `input still staged after ${attempts} re-submit attempts`;
|
|
11274
11514
|
throw new Error(`nudge failed: peer at pane ${target.id} never took the turn — ${why}`);
|
|
11275
11515
|
}
|
|
11276
|
-
|
|
11277
|
-
//#region src/console/doorbell.ts
|
|
11516
|
+
// src/console/doorbell.ts
|
|
11278
11517
|
/** The doorbell text delivered to a woken recipient; also the standalone `unit nudge` default. */
|
|
11279
11518
|
const DELIVERY_DOORBELL = "You have unread mail — check your inbox.";
|
|
11280
11519
|
/**
|
|
@@ -11473,8 +11712,7 @@ async function wakeSpawn(getAdapter, exec, input, nudgeOpts = SPAWN_NUDGE_OPTS,
|
|
|
11473
11712
|
};
|
|
11474
11713
|
}
|
|
11475
11714
|
}
|
|
11476
|
-
|
|
11477
|
-
//#region src/console/trust.ts
|
|
11715
|
+
// src/console/trust.ts
|
|
11478
11716
|
/**
|
|
11479
11717
|
* Each harness's folder-trust prompt, as it draws it, and the keys that accept it. Read from Claude
|
|
11480
11718
|
* Code 2.1.286, Codex 0.153.4 (and its newer "Trust this folder?" wording), and cursor-agent
|
|
@@ -11584,8 +11822,7 @@ function stuck(where) {
|
|
|
11584
11822
|
message: `${where}, and the prompt is still showing after spawn sent the keys that accept it. A person must answer it.`
|
|
11585
11823
|
};
|
|
11586
11824
|
}
|
|
11587
|
-
|
|
11588
|
-
//#region src/mux-select.ts
|
|
11825
|
+
// src/mux-select.ts
|
|
11589
11826
|
/**
|
|
11590
11827
|
* Backend selection via cyber-mux's two-mode mux probe, normalized through the transitional
|
|
11591
11828
|
* `$CYBERLEGION_MUX*` → `$CYBER_MUX*` env seam (`mux-env.ts`) — tmux/herdr map to their existing
|
|
@@ -11605,8 +11842,7 @@ function selectSessionAdapter(env, exec = realExec) {
|
|
|
11605
11842
|
if (probe.mux === "wezterm" || probe.mux === "zellij") throw new Error(`spawn detected ${probe.mux}, a backend unit/registry cannot store a pane locator under (only tmux and herdr) — refusing before opening anything`);
|
|
11606
11843
|
throw new Error("spawn requires a session backend — run inside tmux ($TMUX) or herdr ($HERDR_ENV=1)");
|
|
11607
11844
|
}
|
|
11608
|
-
|
|
11609
|
-
//#region src/workspace-label.ts
|
|
11845
|
+
// src/workspace-label.ts
|
|
11610
11846
|
/** Leading actions that tear down or revert — the `A2-` class, checked first. */
|
|
11611
11847
|
const TEARDOWN_ACTIONS = /* @__PURE__ */ new Set([
|
|
11612
11848
|
"clean",
|
|
@@ -11749,8 +11985,7 @@ function fitWords(words, budget) {
|
|
|
11749
11985
|
}
|
|
11750
11986
|
return out;
|
|
11751
11987
|
}
|
|
11752
|
-
|
|
11753
|
-
//#region src/worktree-lease.ts
|
|
11988
|
+
// src/worktree-lease.ts
|
|
11754
11989
|
/**
|
|
11755
11990
|
* The two worktree-library calls a unit's lifetime makes: `unit spawn` acquires a worktree, `unit
|
|
11756
11991
|
* close` releases it. Gathered here so a test can stand in for them and drive spawn's routing without
|
|
@@ -11774,8 +12009,7 @@ function leaseHolder(id) {
|
|
|
11774
12009
|
* cyberlegion's own file, not the unit's work, and counting it would keep every slot from reuse in a
|
|
11775
12010
|
* project that does not track it. */
|
|
11776
12011
|
const LEASE_IGNORE = [WORKTREE_MARKER];
|
|
11777
|
-
|
|
11778
|
-
//#region src/session.ts
|
|
12012
|
+
// src/session.ts
|
|
11779
12013
|
/** How each harness's own CLI is launched in the new pane. */
|
|
11780
12014
|
const LAUNCH_MAP = {
|
|
11781
12015
|
claude: "claude",
|
|
@@ -12000,10 +12234,7 @@ async function spawn(ctx, input) {
|
|
|
12000
12234
|
cwd,
|
|
12001
12235
|
worktree,
|
|
12002
12236
|
...sharedWorktree ? { sharedWorktree } : {},
|
|
12003
|
-
pane:
|
|
12004
|
-
mux: muxName,
|
|
12005
|
-
id: target.id
|
|
12006
|
-
} : null,
|
|
12237
|
+
pane: openedPaneLocator(exec, muxName, target.id),
|
|
12007
12238
|
status: "active",
|
|
12008
12239
|
createdAt: ts,
|
|
12009
12240
|
lastSeen: ts,
|
|
@@ -12325,8 +12556,7 @@ function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
|
|
|
12325
12556
|
if (input.task != null && input.task !== "") return input.task;
|
|
12326
12557
|
return null;
|
|
12327
12558
|
}
|
|
12328
|
-
|
|
12329
|
-
//#region src/agentdef/resolve.ts
|
|
12559
|
+
// src/agentdef/resolve.ts
|
|
12330
12560
|
function parseAgentDefFile(text) {
|
|
12331
12561
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
|
|
12332
12562
|
if (!m) return {
|
|
@@ -12446,8 +12676,7 @@ function listAgentDefs(input = {}) {
|
|
|
12446
12676
|
}
|
|
12447
12677
|
return defs;
|
|
12448
12678
|
}
|
|
12449
|
-
|
|
12450
|
-
//#region src/agentdef/realize.ts
|
|
12679
|
+
// src/agentdef/realize.ts
|
|
12451
12680
|
const DEFAULT_HARNESS = "claude";
|
|
12452
12681
|
/** POSIX single-quote a value for safe inclusion in a shell command line. */
|
|
12453
12682
|
function shellQuote(value) {
|
|
@@ -12565,8 +12794,7 @@ function resolveSpawnLaunch(input) {
|
|
|
12565
12794
|
...input.searchRoots ? { searchRoots: input.searchRoots } : {}
|
|
12566
12795
|
}), overrides);
|
|
12567
12796
|
}
|
|
12568
|
-
|
|
12569
|
-
//#region src/cli-input.ts
|
|
12797
|
+
// src/cli-input.ts
|
|
12570
12798
|
/**
|
|
12571
12799
|
* Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
|
|
12572
12800
|
* `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
|
|
@@ -12614,8 +12842,7 @@ function spawnCommandInput(opts, listCursorModels) {
|
|
|
12614
12842
|
function readCommandOutput(format, result) {
|
|
12615
12843
|
return format === "json" ? JSON.stringify(result, null, 2) : result.output;
|
|
12616
12844
|
}
|
|
12617
|
-
|
|
12618
|
-
//#region src/cursor-models.ts
|
|
12845
|
+
// src/cursor-models.ts
|
|
12619
12846
|
/** The model ids in `cursor-agent models` output: the first token of each line, ANSI colour and list
|
|
12620
12847
|
* bullets stripped. Headings and tips come along too; callers only test a specific id for membership. */
|
|
12621
12848
|
function parseCursorModels(output) {
|
|
@@ -12638,8 +12865,7 @@ function listCursorModels() {
|
|
|
12638
12865
|
return [];
|
|
12639
12866
|
}
|
|
12640
12867
|
}
|
|
12641
|
-
|
|
12642
|
-
//#region src/message.ts
|
|
12868
|
+
// src/message.ts
|
|
12643
12869
|
/** Write one message into the recipient's inbox. Collision-free by <epochMs>-<hex>. */
|
|
12644
12870
|
function send(ctx, input) {
|
|
12645
12871
|
const toId = resolveRecipient(ctx.store, input.to);
|
|
@@ -12736,8 +12962,7 @@ function deleteMessage(ctx, meId, msgId) {
|
|
|
12736
12962
|
function deleteMailbox(store, id) {
|
|
12737
12963
|
store.removeMailbox(id);
|
|
12738
12964
|
}
|
|
12739
|
-
|
|
12740
|
-
//#region src/decommission.ts
|
|
12965
|
+
// src/decommission.ts
|
|
12741
12966
|
/**
|
|
12742
12967
|
* Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses a
|
|
12743
12968
|
* dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
|
|
@@ -12773,7 +12998,7 @@ async function decommission(ctx, input) {
|
|
|
12773
12998
|
const worktreeExists = worktreeRoot != null && existsSync(worktreeRoot);
|
|
12774
12999
|
const primaryRoot = worktreeExists ? await primaryRootOf(exec, worktreeRoot) : rec.worktree?.primaryRoot;
|
|
12775
13000
|
const held = rec.worktree?.lease;
|
|
12776
|
-
const onPrimary =
|
|
13001
|
+
const onPrimary = isPrimaryCheckout(worktreeRoot, primaryRoot);
|
|
12777
13002
|
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`);
|
|
12778
13003
|
const removesWorktree = worktreeExists && !input.keepWorktree && !held && !onPrimary;
|
|
12779
13004
|
if (removesWorktree && !input.force && isDirty(exec, worktreeRoot)) throw new Error(`unit "${input.id}" has uncommitted changes in its worktree — pass --force to discard them`);
|
|
@@ -12839,10 +13064,9 @@ function isDirty(exec, worktreeRoot) {
|
|
|
12839
13064
|
"--porcelain",
|
|
12840
13065
|
"--untracked-files=all"
|
|
12841
13066
|
]);
|
|
12842
|
-
return !!status && status.split("\n").some((line) => line && line !== `??
|
|
13067
|
+
return !!status && status.split("\n").some((line) => line && line !== `?? .agents/cyberlegion/config.json`);
|
|
12843
13068
|
}
|
|
12844
|
-
|
|
12845
|
-
//#region src/install.ts
|
|
13069
|
+
// src/install.ts
|
|
12846
13070
|
const PIN_TOKEN = /^[0-9A-Za-z][0-9A-Za-z._+-]*$/;
|
|
12847
13071
|
function validatePin(pin) {
|
|
12848
13072
|
if (!PIN_TOKEN.test(pin)) throw new Error(`invalid --pin "${pin}" — expected a version or dist-tag token like 0.2.0 or latest (no spaces, ranges, or shell metacharacters)`);
|
|
@@ -12995,8 +13219,43 @@ function removeCursor(settings, event, canonical) {
|
|
|
12995
13219
|
else delete hooks[event];
|
|
12996
13220
|
return true;
|
|
12997
13221
|
}
|
|
12998
|
-
|
|
12999
|
-
|
|
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
|
+
}
|
|
13258
|
+
// src/output.ts
|
|
13000
13259
|
function stringifyCell(v) {
|
|
13001
13260
|
if (v == null) return "";
|
|
13002
13261
|
const s = String(v);
|
|
@@ -13029,8 +13288,7 @@ function fail(msg) {
|
|
|
13029
13288
|
console.error(JSON.stringify({ error: msg }));
|
|
13030
13289
|
process.exit(1);
|
|
13031
13290
|
}
|
|
13032
|
-
|
|
13033
|
-
//#region src/participants.ts
|
|
13291
|
+
// src/participants.ts
|
|
13034
13292
|
const UNIT_PREFIX = "cyberlegion:unit/";
|
|
13035
13293
|
/** This hub's own participant key. One per hub root, so reconciling one hub (a `--space`, say)
|
|
13036
13294
|
* never retires the units another hub registered in the same cynapse store. */
|
|
@@ -13194,8 +13452,7 @@ async function syncCynapse(hub, env = process.env, load = importCynapse) {
|
|
|
13194
13452
|
cyn?.close();
|
|
13195
13453
|
}
|
|
13196
13454
|
}
|
|
13197
|
-
|
|
13198
|
-
//#region src/permission.ts
|
|
13455
|
+
// src/permission.ts
|
|
13199
13456
|
const CLI_RULE = "Bash(cyberlegion *)";
|
|
13200
13457
|
const COVERING = /^Bash(?:\((?:\*|cyberlegion(?: \*|:\*|\*))\))?$/;
|
|
13201
13458
|
/** Claude Code's user settings file: `$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`. */
|
|
@@ -13246,8 +13503,7 @@ function addPermissionRule(file) {
|
|
|
13246
13503
|
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
13247
13504
|
return "added";
|
|
13248
13505
|
}
|
|
13249
|
-
|
|
13250
|
-
//#region src/presence-spawn.ts
|
|
13506
|
+
// src/presence-spawn.ts
|
|
13251
13507
|
/** The brief a presence spawned into a standing owner's home starts from: whom it stands in for, and
|
|
13252
13508
|
* where that owner's mail is. What to *do* with the mail is the home's agent definition's business. */
|
|
13253
13509
|
function presenceBrief(handle) {
|
|
@@ -13342,8 +13598,7 @@ async function spawnPresence(ctx, owner, options = {}) {
|
|
|
13342
13598
|
...turn
|
|
13343
13599
|
};
|
|
13344
13600
|
}
|
|
13345
|
-
|
|
13346
|
-
//#region src/project.ts
|
|
13601
|
+
// src/project.ts
|
|
13347
13602
|
/**
|
|
13348
13603
|
* The canonical git common dir for `dir`, or undefined outside a repository. The common dir is the
|
|
13349
13604
|
* one path every checkout of a repository shares — the default checkout and each linked worktree
|
|
@@ -13462,8 +13717,7 @@ function byPath(ctx, dir) {
|
|
|
13462
13717
|
if (!commonDir) throw new Error(`"${dir}" is not inside a git repository`);
|
|
13463
13718
|
return ctx.store.getProject(projectIdOf(commonDir)) ?? registerProject(ctx, { dir });
|
|
13464
13719
|
}
|
|
13465
|
-
|
|
13466
|
-
//#region src/runtime/inject-inbox.ts
|
|
13720
|
+
// src/runtime/inject-inbox.ts
|
|
13467
13721
|
const EVENTS = ["SessionStart"];
|
|
13468
13722
|
/**
|
|
13469
13723
|
* Resolve the calling agent, gather its unread mail (and a standing owner's, when this session is
|
|
@@ -13518,8 +13772,7 @@ function injectInbox(ctx, event) {
|
|
|
13518
13772
|
additionalContext: parts.join("\n\n")
|
|
13519
13773
|
} };
|
|
13520
13774
|
}
|
|
13521
|
-
|
|
13522
|
-
//#region src/service.ts
|
|
13775
|
+
// src/service.ts
|
|
13523
13776
|
/** A refused ownership transition. `stale` means the caller's generation, token, or holder claim is
|
|
13524
13777
|
* not the current one — the caller is not (or no longer) the authority it claims to be. */
|
|
13525
13778
|
var ServiceOwnershipError = class extends Error {
|
|
@@ -13824,8 +14077,7 @@ async function startService(ctx, projectRef, name, input) {
|
|
|
13824
14077
|
throw err;
|
|
13825
14078
|
}
|
|
13826
14079
|
}
|
|
13827
|
-
|
|
13828
|
-
//#region src/standing-home.ts
|
|
14080
|
+
// src/standing-home.ts
|
|
13829
14081
|
/**
|
|
13830
14082
|
* Decide what a registration does to a standing owner's home: `undefined` keeps whatever the record
|
|
13831
14083
|
* already has, `null` drops it, and a `StandingHome` replaces it. Every way a home can be wrong is
|
|
@@ -13872,8 +14124,7 @@ function resolveHomeFlags(exec, flags) {
|
|
|
13872
14124
|
...launch
|
|
13873
14125
|
};
|
|
13874
14126
|
}
|
|
13875
|
-
|
|
13876
|
-
//#region src/store/errors.ts
|
|
14127
|
+
// src/store/errors.ts
|
|
13877
14128
|
/** A record file exists but its content didn't parse as JSON — a torn write (crash mid-`writeFileSync`,
|
|
13878
14129
|
* pre-atomic-write code path) or on-disk tampering. Carries the file path and the original parse
|
|
13879
14130
|
* error so a caller can report exactly what's broken and where, rather than "Unexpected token" with
|
|
@@ -13886,8 +14137,7 @@ var CorruptRecordError = class extends Error {
|
|
|
13886
14137
|
this.name = "CorruptRecordError";
|
|
13887
14138
|
}
|
|
13888
14139
|
};
|
|
13889
|
-
|
|
13890
|
-
//#region src/store/process-liveness.ts
|
|
14140
|
+
// src/store/process-liveness.ts
|
|
13891
14141
|
/**
|
|
13892
14142
|
* Probe whether `pid` is alive using `process.kill(pid, 0)` — the POSIX "does this process exist"
|
|
13893
14143
|
* idiom; it sends no signal, it only tests deliverability. `ESRCH` (no such process) is the only
|
|
@@ -13907,8 +14157,7 @@ function probeProcess(pid) {
|
|
|
13907
14157
|
return err.code === "ESRCH" ? "dead" : "unknown";
|
|
13908
14158
|
}
|
|
13909
14159
|
}
|
|
13910
|
-
|
|
13911
|
-
//#region src/store/lock.ts
|
|
14160
|
+
// src/store/lock.ts
|
|
13912
14161
|
var LockTimeoutError = class extends Error {
|
|
13913
14162
|
name;
|
|
13914
14163
|
constructor(name) {
|
|
@@ -14045,8 +14294,7 @@ async function withLockAsync(root, name, fn, opts) {
|
|
|
14045
14294
|
handle.release();
|
|
14046
14295
|
}
|
|
14047
14296
|
}
|
|
14048
|
-
|
|
14049
|
-
//#region src/store/file-store.ts
|
|
14297
|
+
// src/store/file-store.ts
|
|
14050
14298
|
/** Parse a record file's content, wrapping a `JSON.parse` failure in a typed, file-named
|
|
14051
14299
|
* `CorruptRecordError` instead of letting a bare `SyntaxError` bubble up from deep inside
|
|
14052
14300
|
* `listInbox`/`listAgents`/`getAgent` with no indication of WHICH file broke. A file that doesn't
|
|
@@ -14256,8 +14504,7 @@ var FileStore = class {
|
|
|
14256
14504
|
return withLockAsync(this.root, name, fn);
|
|
14257
14505
|
}
|
|
14258
14506
|
};
|
|
14259
|
-
|
|
14260
|
-
//#region src/unit-runtime.ts
|
|
14507
|
+
// src/unit-runtime.ts
|
|
14261
14508
|
/** A standing or service record is an address with no runtime of its own — nothing to stop or start. */
|
|
14262
14509
|
function assertHasRuntime(rec, ref) {
|
|
14263
14510
|
if (rec.kind === "standing" || rec.kind === "service") throw new Error(`"${ref}" is a ${rec.kind} record — it has no runtime to stop, restart, or rebind`);
|
|
@@ -14345,8 +14592,9 @@ function stopUnit(ctx, ref) {
|
|
|
14345
14592
|
* record to the new pane, and ring it. When the record carries the harness's conversation id and the
|
|
14346
14593
|
* harness can resume by id, the session resumes that conversation and is rung to continue, with the
|
|
14347
14594
|
* plain launch as a shell fallback should the harness reject the id; otherwise (or with `fresh`) it
|
|
14348
|
-
* starts empty and is rung to read its brief — a rebrief.
|
|
14349
|
-
*
|
|
14595
|
+
* starts empty and is rung to read its brief — a rebrief. An exited unit is refused: its session
|
|
14596
|
+
* ended without a `unit stop`, and the multiplexer that resumes agents has already brought back any
|
|
14597
|
+
* that were coming back. Every refusal runs before anything is torn down.
|
|
14350
14598
|
*
|
|
14351
14599
|
* The unit is recorded `stopped` before the open, so a restart interrupted between the two leaves an
|
|
14352
14600
|
* ordinary stopped unit and a rerun recovers it; nothing needs a separate repair path.
|
|
@@ -14355,6 +14603,7 @@ async function restartUnit(ctx, ref, options = {}) {
|
|
|
14355
14603
|
const rec = resolveAgent(ctx.store, ref);
|
|
14356
14604
|
assertHasRuntime(rec, ref);
|
|
14357
14605
|
assertNotSelf(ctx, rec, "restart");
|
|
14606
|
+
if (rec.status === "exited") throw new Error(`unit "${rec.handle}" has exited — restart takes only a running or stopped unit; spawn a new unit instead, or run unit rebind ${rec.handle} from a session resumed by hand`);
|
|
14358
14607
|
const harness = rec.harness;
|
|
14359
14608
|
if (!harness || !(harness in LAUNCH_MAP)) throw new Error(`unit "${rec.handle}" has harness "${rec.harness ?? ""}", not in the launch map (${Object.keys(LAUNCH_MAP).join(" | ")}) — cannot restart it`);
|
|
14360
14609
|
if (!existsSync(rec.cwd)) throw new Error(`unit "${rec.handle}" cannot restart — its cwd ${rec.cwd} is gone; spawn a new unit instead`);
|
|
@@ -14396,10 +14645,7 @@ async function restartUnit(ctx, ref, options = {}) {
|
|
|
14396
14645
|
const bound = {
|
|
14397
14646
|
...stopped,
|
|
14398
14647
|
status: "active",
|
|
14399
|
-
pane:
|
|
14400
|
-
mux,
|
|
14401
|
-
id: target.id
|
|
14402
|
-
} : null,
|
|
14648
|
+
pane: openedPaneLocator(exec, mux, target.id),
|
|
14403
14649
|
lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
|
|
14404
14650
|
};
|
|
14405
14651
|
saveAgent(ctx.store, bound);
|
|
@@ -14425,6 +14671,10 @@ function probeLiveness(ctx, rec) {
|
|
|
14425
14671
|
if (rec.kind === "standing" || rec.kind === "service") return "none";
|
|
14426
14672
|
if (rec.status === "stopped") return "stopped";
|
|
14427
14673
|
if (rec.status === "exited") return "exited";
|
|
14674
|
+
if (rec.pane?.mux === "herdr" && rec.pane.terminal) {
|
|
14675
|
+
const at = locateTerminal(ctx.exec ?? realExec, rec.pane.terminal);
|
|
14676
|
+
return at === void 0 ? "unknown" : at === "gone" ? "gone" : "live";
|
|
14677
|
+
}
|
|
14428
14678
|
const pane = runtimePane(ctx, rec);
|
|
14429
14679
|
const adapter = pane ? paneAdapter(ctx, rec) : void 0;
|
|
14430
14680
|
if (!pane || !adapter) return "unknown";
|
|
@@ -14441,7 +14691,7 @@ function probeLiveness(ctx, rec) {
|
|
|
14441
14691
|
function rebindUnit(ctx, ref) {
|
|
14442
14692
|
const rec = resolveAgent(ctx.store, ref);
|
|
14443
14693
|
assertHasRuntime(rec, ref);
|
|
14444
|
-
const cur =
|
|
14694
|
+
const cur = callingStorablePane(ctx);
|
|
14445
14695
|
if (!cur) throw new Error("unit rebind needs to run inside a tmux or herdr pane — there is no pane to bind");
|
|
14446
14696
|
const oldPane = runtimePane(ctx, rec);
|
|
14447
14697
|
if (rec.pane?.mux === cur.mux && rec.pane.id === cur.pane && ctx.store.resolvePaneId(cur.pane) === rec.id) return rec;
|
|
@@ -14449,13 +14699,15 @@ function rebindUnit(ctx, ref) {
|
|
|
14449
14699
|
const holder = holderId && holderId !== rec.id ? loadAgent(ctx.store, holderId) : void 0;
|
|
14450
14700
|
if (holder && holder.status !== "stopped" && holder.status !== "exited") throw new Error(`this pane belongs to unit "${holder.handle}" (${holder.id}) — rebind never takes another unit's pane`);
|
|
14451
14701
|
const liveness = probeLiveness(ctx, rec);
|
|
14452
|
-
|
|
14702
|
+
const ownTerminal = !!cur.terminal && rec.pane?.mux === "herdr" && rec.pane.terminal === cur.terminal;
|
|
14703
|
+
if (oldPane && oldPane !== cur.pane && !ownTerminal && (liveness === "live" || liveness === "unknown")) throw new Error(`unit "${rec.handle}" may still be running in pane ${oldPane} — stop it first (unit stop ${rec.handle})`);
|
|
14453
14704
|
const bound = {
|
|
14454
14705
|
...rec,
|
|
14455
14706
|
status: "active",
|
|
14456
14707
|
pane: {
|
|
14457
14708
|
mux: cur.mux,
|
|
14458
|
-
id: cur.pane
|
|
14709
|
+
id: cur.pane,
|
|
14710
|
+
...cur.terminal ? { terminal: cur.terminal } : {}
|
|
14459
14711
|
},
|
|
14460
14712
|
lastSeen: new Date(ctx.now?.() ?? Date.now()).toISOString()
|
|
14461
14713
|
};
|
|
@@ -14483,7 +14735,7 @@ function controlsFor(ctx, rec, liveness) {
|
|
|
14483
14735
|
if (hasHonestReset(rec.harness)) controls.push("clear");
|
|
14484
14736
|
}
|
|
14485
14737
|
if (runtimePane(ctx, rec) !== void 0 && (liveness === "live" || liveness === "gone" || liveness === "unknown")) controls.push("stop");
|
|
14486
|
-
if (rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
|
|
14738
|
+
if (liveness !== "exited" && rec.harness && rec.harness in LAUNCH_MAP && existsSync(rec.cwd)) controls.push("restart");
|
|
14487
14739
|
if (liveness === "stopped" || liveness === "exited" || liveness === "gone") controls.push("rebind");
|
|
14488
14740
|
controls.push("close");
|
|
14489
14741
|
return controls;
|
|
@@ -14538,8 +14790,7 @@ function recordConversation(ctx, hookInput) {
|
|
|
14538
14790
|
conversation: id
|
|
14539
14791
|
});
|
|
14540
14792
|
}
|
|
14541
|
-
|
|
14542
|
-
//#region src/wake/await.ts
|
|
14793
|
+
// src/wake/await.ts
|
|
14543
14794
|
/** Internal poll interval — not user-facing (the round-trip cost of one `mail await` cycle). */
|
|
14544
14795
|
const POLL_MS$1 = 1e3;
|
|
14545
14796
|
const DEFAULT_TIMEOUT_MS = 6e5;
|
|
@@ -14584,8 +14835,7 @@ async function awaitReply(ctx, input) {
|
|
|
14584
14835
|
await sleep(POLL_MS$1);
|
|
14585
14836
|
}
|
|
14586
14837
|
}
|
|
14587
|
-
|
|
14588
|
-
//#region src/wake/watch.ts
|
|
14838
|
+
// src/wake/watch.ts
|
|
14589
14839
|
const POLL_MS = 1e3;
|
|
14590
14840
|
/**
|
|
14591
14841
|
* A continuous foreground observer: prints each NEW matching message as it arrives, polling on a
|
|
@@ -14617,8 +14867,7 @@ async function watchMail(ctx, input, onMessage, opts = {}) {
|
|
|
14617
14867
|
await sleep(POLL_MS);
|
|
14618
14868
|
}
|
|
14619
14869
|
}
|
|
14620
|
-
|
|
14621
|
-
//#region src/cli.ts
|
|
14870
|
+
// src/cli.ts
|
|
14622
14871
|
const VERSION = globalThis.__SKILL_SCRIPT__?.version ?? JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
14623
14872
|
function ctxOf(opts) {
|
|
14624
14873
|
return {
|
|
@@ -14821,18 +15070,25 @@ function runWho(opts) {
|
|
|
14821
15070
|
if (agents.length === 0) nextStep("cyberlegion unit register to join");
|
|
14822
15071
|
}
|
|
14823
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);
|
|
14824
|
-
syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and
|
|
15073
|
+
syncsUnits(withGlobals(unit.command("prune"))).description("mark dead units exited and delete exited units that leave no work on disk").action(async (opts) => {
|
|
14825
15074
|
const ctx = ctxOf(opts);
|
|
14826
15075
|
touch(ctx);
|
|
14827
|
-
const changed = prune(ctx);
|
|
15076
|
+
const changed = prune(ctx, { landed: await landedWorktrees(ctx) });
|
|
14828
15077
|
emit(formatOf(opts), {
|
|
14829
|
-
toon: toonList("pruned", changed, [
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
15078
|
+
toon: toonList("pruned", changed, [
|
|
15079
|
+
{
|
|
15080
|
+
key: "id",
|
|
15081
|
+
get: (a) => a.id
|
|
15082
|
+
},
|
|
15083
|
+
{
|
|
15084
|
+
key: "handle",
|
|
15085
|
+
get: (a) => a.handle
|
|
15086
|
+
},
|
|
15087
|
+
{
|
|
15088
|
+
key: "pruned",
|
|
15089
|
+
get: (a) => a.pruned
|
|
15090
|
+
}
|
|
15091
|
+
], `${changed.length} pruned (${changed.filter((a) => a.pruned === "deleted").length} deleted)`),
|
|
14836
15092
|
json: changed
|
|
14837
15093
|
});
|
|
14838
15094
|
});
|
|
@@ -15819,5 +16075,4 @@ async function runCli(argv = process.argv) {
|
|
|
15819
16075
|
process.exit(1);
|
|
15820
16076
|
}
|
|
15821
16077
|
}
|
|
15822
|
-
//#endregion
|
|
15823
16078
|
export { runCli };
|