cyberlegion 0.4.0 → 0.5.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/bin/cyberlegion.mjs +18 -1
- package/dist/cli.mjs +267 -146
- package/dist/index.d.mts +19 -4
- package/dist/index.mjs +99 -15
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/readme.md +6 -3
- package/skills/dispatch-governance/SKILL.md +9 -4
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +31 -0
- package/skills/init-cyberlegion/SKILL.md +16 -14
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +31 -0
- package/skills/legate/SKILL.md +14 -14
- package/skills/legate/scripts/cyberlegion.mjs +31 -0
- package/skills/manage-inbox/SKILL.md +27 -14
- package/skills/manage-inbox/scripts/cyberlegion.mjs +31 -0
- package/skills/relay-governance/README.md +2 -2
- package/skills/relay-governance/SKILL.md +7 -2
- package/skills/relay-governance/scripts/cyberlegion.mjs +31 -0
- package/skills/subagent-backend-governance/SKILL.md +6 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +31 -0
package/.plugin/pins.json
CHANGED
package/bin/cyberlegion.mjs
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
3
|
import { dirname, join } from 'node:path'
|
|
3
4
|
import { fileURLToPath } from 'node:url'
|
|
4
5
|
|
|
5
6
|
const dir = dirname(fileURLToPath(import.meta.url))
|
|
6
|
-
const
|
|
7
|
+
const cli = join(dir, '..', 'dist', 'cli.mjs')
|
|
8
|
+
|
|
9
|
+
// A plugin installed from a source checkout carries whatever the checkout carries, and no install
|
|
10
|
+
// step runs after the copy. Without the built CLI, a bare import dies with ERR_MODULE_NOT_FOUND and
|
|
11
|
+
// no way forward; say what is missing and name the exact published version that runs instead.
|
|
12
|
+
if (!existsSync(cli)) {
|
|
13
|
+
const { version } = JSON.parse(readFileSync(join(dir, '..', 'package.json'), 'utf8'))
|
|
14
|
+
process.stderr.write(
|
|
15
|
+
`cyberlegion: the built CLI is missing: ${cli}\n` +
|
|
16
|
+
'This install has no dist/ — it is a source checkout that was never built.\n' +
|
|
17
|
+
`Run the published CLI of the same version instead: npx -y cyberlegion@${version}\n` +
|
|
18
|
+
`Or build it in place: pnpm install && pnpm build (in ${join(dir, '..')})\n`,
|
|
19
|
+
)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { runCli } = await import(cli)
|
|
7
24
|
await runCli()
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
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
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -3170,12 +3170,12 @@ function assertDistinctFromPrimary(worktreeRoot, primaryRoot) {
|
|
|
3170
3170
|
* Without this a value carrying a space or a quote would split into extra words, or unbalance the
|
|
3171
3171
|
* line outright.
|
|
3172
3172
|
*/
|
|
3173
|
-
function shellQuote$
|
|
3173
|
+
function shellQuote$2(value) {
|
|
3174
3174
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3175
3175
|
}
|
|
3176
3176
|
/** `env K=V …` with a trailing space, ready to prepend to a command line. Values are shell-quoted. */
|
|
3177
3177
|
function envPrefix(env) {
|
|
3178
|
-
return `env ${Object.entries(env).map(([key, value]) => `${key}=${shellQuote$
|
|
3178
|
+
return `env ${Object.entries(env).map(([key, value]) => `${key}=${shellQuote$2(value)}`).join(" ")} `;
|
|
3179
3179
|
}
|
|
3180
3180
|
/**
|
|
3181
3181
|
* Given the env a route could not carry and the command (if any) that would run in the opened pane,
|
|
@@ -5080,119 +5080,6 @@ function sanitizePane(pane) {
|
|
|
5080
5080
|
return pane.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
5081
5081
|
}
|
|
5082
5082
|
//#endregion
|
|
5083
|
-
//#region src/agentdef/resolve.ts
|
|
5084
|
-
function parseAgentDefFile(text) {
|
|
5085
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
|
|
5086
|
-
if (!m) return {
|
|
5087
|
-
fm: {},
|
|
5088
|
-
instructions: text.trim()
|
|
5089
|
-
};
|
|
5090
|
-
const [, block, rest] = m;
|
|
5091
|
-
const fm = {};
|
|
5092
|
-
const lines = block.split("\n");
|
|
5093
|
-
let i = 0;
|
|
5094
|
-
while (i < lines.length) {
|
|
5095
|
-
const line = lines[i].replace(/\r$/, "");
|
|
5096
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
5097
|
-
i++;
|
|
5098
|
-
continue;
|
|
5099
|
-
}
|
|
5100
|
-
if (line.length - line.trimStart().length > 0) {
|
|
5101
|
-
i++;
|
|
5102
|
-
continue;
|
|
5103
|
-
}
|
|
5104
|
-
const idx = line.indexOf(":");
|
|
5105
|
-
if (idx === -1) {
|
|
5106
|
-
i++;
|
|
5107
|
-
continue;
|
|
5108
|
-
}
|
|
5109
|
-
const key = line.slice(0, idx).trim();
|
|
5110
|
-
let value = line.slice(idx + 1).trim();
|
|
5111
|
-
i++;
|
|
5112
|
-
if (value === ">" || value === ">-" || value === "|" || value === "|-") {
|
|
5113
|
-
const parts = [];
|
|
5114
|
-
while (i < lines.length) {
|
|
5115
|
-
const l = lines[i].replace(/\r$/, "");
|
|
5116
|
-
if (l.trim() === "") {
|
|
5117
|
-
i++;
|
|
5118
|
-
continue;
|
|
5119
|
-
}
|
|
5120
|
-
if (l.length - l.trimStart().length === 0) break;
|
|
5121
|
-
parts.push(l.trim());
|
|
5122
|
-
i++;
|
|
5123
|
-
}
|
|
5124
|
-
value = parts.join(value.startsWith("|") ? "\n" : " ");
|
|
5125
|
-
}
|
|
5126
|
-
fm[key] = unquote(value);
|
|
5127
|
-
}
|
|
5128
|
-
return {
|
|
5129
|
-
fm,
|
|
5130
|
-
instructions: rest.trim()
|
|
5131
|
-
};
|
|
5132
|
-
}
|
|
5133
|
-
function unquote(v) {
|
|
5134
|
-
if (v.length >= 2 && (v.startsWith("\"") && v.endsWith("\"") || v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1);
|
|
5135
|
-
return v;
|
|
5136
|
-
}
|
|
5137
|
-
function toBool(v) {
|
|
5138
|
-
if (v === void 0) return void 0;
|
|
5139
|
-
if (v === "true") return true;
|
|
5140
|
-
if (v === "false") return false;
|
|
5141
|
-
}
|
|
5142
|
-
function toAgentDef(path, text, fallbackName) {
|
|
5143
|
-
const { fm, instructions } = parseAgentDefFile(text);
|
|
5144
|
-
return {
|
|
5145
|
-
name: fm.name ?? fallbackName,
|
|
5146
|
-
description: fm.description,
|
|
5147
|
-
model: fm.model,
|
|
5148
|
-
effort: fm.effort,
|
|
5149
|
-
harness: fm.harness,
|
|
5150
|
-
warm: toBool(fm.warm),
|
|
5151
|
-
interactive: toBool(fm.interactive),
|
|
5152
|
-
instructions,
|
|
5153
|
-
path
|
|
5154
|
-
};
|
|
5155
|
-
}
|
|
5156
|
-
/** The project's own `.agents/agents/` dir — the convention `agent list`/name-search scans. */
|
|
5157
|
-
function projectAgentsDir(cwd) {
|
|
5158
|
-
return join(projectRoot(cwd), ".agents", "agents");
|
|
5159
|
-
}
|
|
5160
|
-
/** Resolve one agent definition, by explicit file or by name search. Throws a clear error when
|
|
5161
|
-
* neither locates a def — never returns a partial/empty result. */
|
|
5162
|
-
function resolveAgentDef(input) {
|
|
5163
|
-
if (input.file) {
|
|
5164
|
-
const path = resolve(input.file);
|
|
5165
|
-
if (!existsSync(path)) throw new Error(`agent def file "${input.file}" does not exist`);
|
|
5166
|
-
return toAgentDef(path, readFileSync(path, "utf8"), basename(path).replace(/\.md$/, ""));
|
|
5167
|
-
}
|
|
5168
|
-
if (!input.name) throw new Error("resolveAgentDef needs a --agent <name> or --agent-file <path>");
|
|
5169
|
-
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
5170
|
-
for (const root of roots) {
|
|
5171
|
-
const candidate = join(root, `${input.name}.md`);
|
|
5172
|
-
if (existsSync(candidate)) return toAgentDef(candidate, readFileSync(candidate, "utf8"), input.name);
|
|
5173
|
-
}
|
|
5174
|
-
throw new Error(`no agent definition named "${input.name}" found under .agents/agents/`);
|
|
5175
|
-
}
|
|
5176
|
-
/** List every resolvable agent def under `.agents/agents/` (+ any extra search roots). Definitive
|
|
5177
|
-
* empty array when the dir is absent or empty — never throws for "none found" here (unlike a
|
|
5178
|
-
* named resolve, listing is inherently zero-or-more). */
|
|
5179
|
-
function listAgentDefs(input = {}) {
|
|
5180
|
-
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
5181
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5182
|
-
const defs = [];
|
|
5183
|
-
for (const root of roots) {
|
|
5184
|
-
if (!existsSync(root)) continue;
|
|
5185
|
-
for (const file of readdirSync(root)) {
|
|
5186
|
-
if (!file.endsWith(".md")) continue;
|
|
5187
|
-
const path = join(root, file);
|
|
5188
|
-
if (seen.has(path)) continue;
|
|
5189
|
-
seen.add(path);
|
|
5190
|
-
defs.push(toAgentDef(path, readFileSync(path, "utf8"), file.replace(/\.md$/, "")));
|
|
5191
|
-
}
|
|
5192
|
-
}
|
|
5193
|
-
return defs;
|
|
5194
|
-
}
|
|
5195
|
-
//#endregion
|
|
5196
5083
|
//#region src/mux-env.ts
|
|
5197
5084
|
/**
|
|
5198
5085
|
* Transitional env-normalization seam (mux.feature: "a pane carrying only the legacy fast-path vars
|
|
@@ -6005,7 +5892,10 @@ function spawn(ctx, input) {
|
|
|
6005
5892
|
const id = randomId();
|
|
6006
5893
|
const primaryRoot = resolvePrimaryRoot(exec);
|
|
6007
5894
|
const launch = input.command ?? LAUNCH_MAP[harness];
|
|
6008
|
-
const
|
|
5895
|
+
const launchLine = () => {
|
|
5896
|
+
const shimDir = ctx.self ? writeSelfShim(paths.dataDir(ctx.store.root, id), ctx.self) : void 0;
|
|
5897
|
+
return `${shimDir ? `PATH=${shellQuote$1(shimDir)}:"$PATH" ` : ""}${muxEnvPrefix(sessionAdapter.name)}${launch}`;
|
|
5898
|
+
};
|
|
6009
5899
|
const from = callerPane(sessionAdapter, normalizedEnv);
|
|
6010
5900
|
let cwd;
|
|
6011
5901
|
let worktree;
|
|
@@ -6018,7 +5908,7 @@ function spawn(ctx, input) {
|
|
|
6018
5908
|
const at = input.at ?? "tab";
|
|
6019
5909
|
target = sessionAdapter.open(exec, {
|
|
6020
5910
|
cwd,
|
|
6021
|
-
launch:
|
|
5911
|
+
launch: launchLine(),
|
|
6022
5912
|
at,
|
|
6023
5913
|
from,
|
|
6024
5914
|
...labelFor(at, input, brief, id)
|
|
@@ -6033,7 +5923,7 @@ function spawn(ctx, input) {
|
|
|
6033
5923
|
primaryRoot,
|
|
6034
5924
|
branch,
|
|
6035
5925
|
path: worktreePath,
|
|
6036
|
-
launch:
|
|
5926
|
+
launch: launchLine(),
|
|
6037
5927
|
...labelFor(at, input, brief, id)
|
|
6038
5928
|
});
|
|
6039
5929
|
assertDistinctFromPrimary(opened.worktree.root, primaryRoot);
|
|
@@ -6053,7 +5943,7 @@ function spawn(ctx, input) {
|
|
|
6053
5943
|
worktree = added;
|
|
6054
5944
|
target = sessionAdapter.open(exec, {
|
|
6055
5945
|
cwd,
|
|
6056
|
-
launch:
|
|
5946
|
+
launch: launchLine(),
|
|
6057
5947
|
at,
|
|
6058
5948
|
from,
|
|
6059
5949
|
...labelFor(at, input, brief, id)
|
|
@@ -6080,7 +5970,7 @@ function spawn(ctx, input) {
|
|
|
6080
5970
|
};
|
|
6081
5971
|
saveAgent(ctx.store, rec);
|
|
6082
5972
|
ctx.store.putPaneIndex(target.id, id);
|
|
6083
|
-
ctx.store.writeBrief(id, brief);
|
|
5973
|
+
ctx.store.writeBrief(id, composeBrief(brief, input.briefInstructions));
|
|
6084
5974
|
return {
|
|
6085
5975
|
agent: rec,
|
|
6086
5976
|
pane: target.id,
|
|
@@ -6148,6 +6038,37 @@ function muxEnvPrefix(muxName) {
|
|
|
6148
6038
|
return "";
|
|
6149
6039
|
}
|
|
6150
6040
|
/**
|
|
6041
|
+
* The argv that re-invokes the running CLI exactly: this node binary, its loader flags (tsx under
|
|
6042
|
+
* `pnpm cl dev`), and the entry script with symlinks resolved — so an `npx` or `.bin` link reaches
|
|
6043
|
+
* the same file it ran from, never whatever the name resolves to later.
|
|
6044
|
+
*/
|
|
6045
|
+
function selfInvocation() {
|
|
6046
|
+
const entry = process.argv[1];
|
|
6047
|
+
return [
|
|
6048
|
+
process.execPath,
|
|
6049
|
+
...process.execArgv,
|
|
6050
|
+
...entry ? [realpathSync(entry)] : []
|
|
6051
|
+
];
|
|
6052
|
+
}
|
|
6053
|
+
/**
|
|
6054
|
+
* Write `<dataDir>/bin/cyberlegion`, a POSIX shim that execs `self` with the caller's arguments,
|
|
6055
|
+
* and return its directory. A spawned session gets that directory first on its PATH, so the
|
|
6056
|
+
* `cyberlegion` a brief tells it to run is the install that spawned it: nothing is resolved from
|
|
6057
|
+
* the registry or the session's own PATH at report time.
|
|
6058
|
+
*/
|
|
6059
|
+
function writeSelfShim(dataDir, self) {
|
|
6060
|
+
const dir = join(dataDir, "bin");
|
|
6061
|
+
mkdirSync(dir, { recursive: true });
|
|
6062
|
+
const shim = join(dir, "cyberlegion");
|
|
6063
|
+
writeFileSync(shim, `#!/bin/sh\nexec ${self.map(shellQuote$1).join(" ")} "$@"\n`);
|
|
6064
|
+
chmodSync(shim, 493);
|
|
6065
|
+
return dir;
|
|
6066
|
+
}
|
|
6067
|
+
/** Single-quote `s` for a POSIX shell, so any path survives word splitting and expansion. */
|
|
6068
|
+
function shellQuote$1(s) {
|
|
6069
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
6070
|
+
}
|
|
6071
|
+
/**
|
|
6151
6072
|
* Resolve a unit's live session pane from a ref, or throw naming the ref. The one place
|
|
6152
6073
|
* focus/nudge/read agree on what "addressable" means — a record's own pane, else a pane pointer
|
|
6153
6074
|
* keyed by its id (a herdr peer stores its pane only in that index).
|
|
@@ -6230,6 +6151,13 @@ function clearUnit(ctx, ref) {
|
|
|
6230
6151
|
command
|
|
6231
6152
|
};
|
|
6232
6153
|
}
|
|
6154
|
+
/** The brief file's text: the task alone, or — when the harness took no instructions on its command
|
|
6155
|
+
* line — the def's instructions under their own heading ahead of it, so the peer can see they reach
|
|
6156
|
+
* it as a user turn rather than a system prompt. */
|
|
6157
|
+
function composeBrief(brief, instructions) {
|
|
6158
|
+
if (!instructions) return brief;
|
|
6159
|
+
return `## Agent instructions\n\n${instructions}\n\n## Brief\n\n${brief}`;
|
|
6160
|
+
}
|
|
6233
6161
|
/** Resolve a spawn brief from --brief-file, --task -, or --task <text>; null if no source given. */
|
|
6234
6162
|
function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
|
|
6235
6163
|
if (input.briefFile) return readFileSync(input.briefFile, "utf8");
|
|
@@ -6238,69 +6166,251 @@ function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
|
|
|
6238
6166
|
return null;
|
|
6239
6167
|
}
|
|
6240
6168
|
//#endregion
|
|
6169
|
+
//#region src/agentdef/resolve.ts
|
|
6170
|
+
function parseAgentDefFile(text) {
|
|
6171
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n)?([\s\S]*)$/.exec(text);
|
|
6172
|
+
if (!m) return {
|
|
6173
|
+
fm: {},
|
|
6174
|
+
instructions: text.trim()
|
|
6175
|
+
};
|
|
6176
|
+
const [, block, rest] = m;
|
|
6177
|
+
const fm = {};
|
|
6178
|
+
const lines = block.split("\n");
|
|
6179
|
+
let i = 0;
|
|
6180
|
+
while (i < lines.length) {
|
|
6181
|
+
const line = lines[i].replace(/\r$/, "");
|
|
6182
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
6183
|
+
i++;
|
|
6184
|
+
continue;
|
|
6185
|
+
}
|
|
6186
|
+
if (line.length - line.trimStart().length > 0) {
|
|
6187
|
+
i++;
|
|
6188
|
+
continue;
|
|
6189
|
+
}
|
|
6190
|
+
const idx = line.indexOf(":");
|
|
6191
|
+
if (idx === -1) {
|
|
6192
|
+
i++;
|
|
6193
|
+
continue;
|
|
6194
|
+
}
|
|
6195
|
+
const key = line.slice(0, idx).trim();
|
|
6196
|
+
let value = line.slice(idx + 1).trim();
|
|
6197
|
+
i++;
|
|
6198
|
+
if (value === ">" || value === ">-" || value === "|" || value === "|-") {
|
|
6199
|
+
const parts = [];
|
|
6200
|
+
while (i < lines.length) {
|
|
6201
|
+
const l = lines[i].replace(/\r$/, "");
|
|
6202
|
+
if (l.trim() === "") {
|
|
6203
|
+
i++;
|
|
6204
|
+
continue;
|
|
6205
|
+
}
|
|
6206
|
+
if (l.length - l.trimStart().length === 0) break;
|
|
6207
|
+
parts.push(l.trim());
|
|
6208
|
+
i++;
|
|
6209
|
+
}
|
|
6210
|
+
value = parts.join(value.startsWith("|") ? "\n" : " ");
|
|
6211
|
+
}
|
|
6212
|
+
fm[key] = unquote(value);
|
|
6213
|
+
}
|
|
6214
|
+
return {
|
|
6215
|
+
fm,
|
|
6216
|
+
instructions: rest.trim()
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
function unquote(v) {
|
|
6220
|
+
if (v.length >= 2 && (v.startsWith("\"") && v.endsWith("\"") || v.startsWith("'") && v.endsWith("'"))) return v.slice(1, -1);
|
|
6221
|
+
return v;
|
|
6222
|
+
}
|
|
6223
|
+
function toBool(v) {
|
|
6224
|
+
if (v === void 0) return void 0;
|
|
6225
|
+
if (v === "true") return true;
|
|
6226
|
+
if (v === "false") return false;
|
|
6227
|
+
}
|
|
6228
|
+
/** The known harnesses are exactly those with a launch binary — one home for the set. */
|
|
6229
|
+
const HARNESSES = Object.keys(LAUNCH_MAP);
|
|
6230
|
+
/** A `harness` tag outside the known set has no launch binary, so a typo fails here, loudly, rather
|
|
6231
|
+
* than surfacing later as an unlaunchable command. */
|
|
6232
|
+
function toHarness(value, path) {
|
|
6233
|
+
if (value === void 0 || HARNESSES.includes(value)) return value;
|
|
6234
|
+
throw new Error(`agent def "${path}" has unknown harness "${value}" — expected one of ${HARNESSES.join(", ")}`);
|
|
6235
|
+
}
|
|
6236
|
+
function toAgentDef(path, text, fallbackName) {
|
|
6237
|
+
const { fm, instructions } = parseAgentDefFile(text);
|
|
6238
|
+
return {
|
|
6239
|
+
name: fm.name ?? fallbackName,
|
|
6240
|
+
description: fm.description,
|
|
6241
|
+
model: fm.model,
|
|
6242
|
+
effort: fm.effort,
|
|
6243
|
+
harness: toHarness(fm.harness, path),
|
|
6244
|
+
warm: toBool(fm.warm),
|
|
6245
|
+
interactive: toBool(fm.interactive),
|
|
6246
|
+
instructions,
|
|
6247
|
+
path
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
/** The project's own `.agents/agents/` dir — the convention `agent list`/name-search scans. */
|
|
6251
|
+
function projectAgentsDir(cwd) {
|
|
6252
|
+
return join(projectRoot(cwd), ".agents", "agents");
|
|
6253
|
+
}
|
|
6254
|
+
/** Resolve one agent definition, by explicit file or by name search. Throws a clear error when
|
|
6255
|
+
* neither locates a def — never returns a partial/empty result. */
|
|
6256
|
+
function resolveAgentDef(input) {
|
|
6257
|
+
if (input.file) {
|
|
6258
|
+
const path = resolve(input.file);
|
|
6259
|
+
if (!existsSync(path)) throw new Error(`agent def file "${input.file}" does not exist`);
|
|
6260
|
+
return toAgentDef(path, readFileSync(path, "utf8"), basename(path).replace(/\.md$/, ""));
|
|
6261
|
+
}
|
|
6262
|
+
if (!input.name) throw new Error("resolveAgentDef needs a --agent <name> or --agent-file <path>");
|
|
6263
|
+
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
6264
|
+
for (const root of roots) {
|
|
6265
|
+
const candidate = join(root, `${input.name}.md`);
|
|
6266
|
+
if (existsSync(candidate)) return toAgentDef(candidate, readFileSync(candidate, "utf8"), input.name);
|
|
6267
|
+
}
|
|
6268
|
+
throw new Error(`no agent definition named "${input.name}" found under .agents/agents/`);
|
|
6269
|
+
}
|
|
6270
|
+
/** List every resolvable agent def under `.agents/agents/` (+ any extra search roots). Definitive
|
|
6271
|
+
* empty array when the dir is absent or empty — never throws for "none found" here (unlike a
|
|
6272
|
+
* named resolve, listing is inherently zero-or-more). */
|
|
6273
|
+
function listAgentDefs(input = {}) {
|
|
6274
|
+
const roots = [...input.searchRoots ?? [], projectAgentsDir(input.cwd)];
|
|
6275
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6276
|
+
const defs = [];
|
|
6277
|
+
for (const root of roots) {
|
|
6278
|
+
if (!existsSync(root)) continue;
|
|
6279
|
+
for (const file of readdirSync(root)) {
|
|
6280
|
+
if (!file.endsWith(".md")) continue;
|
|
6281
|
+
const path = join(root, file);
|
|
6282
|
+
if (seen.has(path)) continue;
|
|
6283
|
+
seen.add(path);
|
|
6284
|
+
defs.push(toAgentDef(path, readFileSync(path, "utf8"), file.replace(/\.md$/, "")));
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
return defs;
|
|
6288
|
+
}
|
|
6289
|
+
//#endregion
|
|
6241
6290
|
//#region src/agentdef/realize.ts
|
|
6242
6291
|
const DEFAULT_HARNESS = "claude";
|
|
6243
6292
|
/** POSIX single-quote a value for safe inclusion in a shell command line. */
|
|
6244
6293
|
function shellQuote(value) {
|
|
6245
6294
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6246
6295
|
}
|
|
6247
|
-
/**
|
|
6248
|
-
*
|
|
6249
|
-
|
|
6250
|
-
|
|
6296
|
+
/** Cursor carries effort as a bracket parameter on the model (`<model>[effort=<level>]`): merge it into
|
|
6297
|
+
* any bracket list the model already has, replacing an `effort=` already there. */
|
|
6298
|
+
function withCursorEffort(model, effort) {
|
|
6299
|
+
const open = model.lastIndexOf("[");
|
|
6300
|
+
if (open === -1 || !model.endsWith("]")) return `${model}[effort=${effort}]`;
|
|
6301
|
+
const params = model.slice(open + 1, -1).split(",").filter((p) => p !== "" && !p.startsWith("effort="));
|
|
6302
|
+
return `${model.slice(0, open)}[${[...params, `effort=${effort}`].join(",")}]`;
|
|
6303
|
+
}
|
|
6304
|
+
/** The model + effort arguments for one harness. No two harnesses spell effort alike: claude has
|
|
6305
|
+
* `--effort`, codex only a config override, cursor only a parameter on the model — so a cursor
|
|
6306
|
+
* effort with no model has nowhere to go and throws rather than launching at the default effort. */
|
|
6307
|
+
function modelAndEffortArgs(harness, model, effort) {
|
|
6308
|
+
if (harness === "cursor") {
|
|
6309
|
+
if (effort && !model) throw new Error(`cursor carries effort only as a parameter on the model; set a model to launch with effort "${effort}"`);
|
|
6310
|
+
if (!model) return [];
|
|
6311
|
+
return ["--model", shellQuote(effort ? withCursorEffort(model, effort) : model)];
|
|
6312
|
+
}
|
|
6313
|
+
const args = model ? ["--model", shellQuote(model)] : [];
|
|
6314
|
+
if (!effort) return args;
|
|
6315
|
+
if (harness === "codex") return [
|
|
6316
|
+
...args,
|
|
6317
|
+
"-c",
|
|
6318
|
+
shellQuote(`model_reasoning_effort="${effort}"`)
|
|
6319
|
+
];
|
|
6320
|
+
return [
|
|
6321
|
+
...args,
|
|
6322
|
+
"--effort",
|
|
6323
|
+
shellQuote(effort)
|
|
6324
|
+
];
|
|
6325
|
+
}
|
|
6326
|
+
/** A TOML basic string: JSON's escapes are all legal TOML escapes, and TOML also forbids a raw DEL. */
|
|
6327
|
+
function tomlBasicString(value) {
|
|
6328
|
+
return JSON.stringify(value).replace(/\x7f/g, "\\u007f");
|
|
6329
|
+
}
|
|
6330
|
+
/** The instruction arguments for one harness. Only claude has an append-to-system-prompt flag;
|
|
6331
|
+
* codex takes a `developer_instructions` config override, added on top of its own base
|
|
6332
|
+
* instructions. Cursor has no channel at all — its instructions go to the brief instead. */
|
|
6333
|
+
function instructionArgs(harness, instructions) {
|
|
6334
|
+
if (!instructions || harness === "cursor") return [];
|
|
6335
|
+
if (harness === "claude") return ["--append-system-prompt", shellQuote(instructions)];
|
|
6336
|
+
return ["-c", shellQuote(`developer_instructions=${tomlBasicString(instructions)}`)];
|
|
6337
|
+
}
|
|
6338
|
+
/** Build the harness launch invocation for a def — explicit `model`/`effort`/`harness` win over the
|
|
6339
|
+
* def's own tags, which win over the harness default. The def's effort and instructions each go
|
|
6340
|
+
* through the harness's own control (see `modelAndEffortArgs`, `instructionArgs`). */
|
|
6251
6341
|
function realizeLaunch(def, opts = {}) {
|
|
6252
6342
|
const harness = opts.harness ?? def.harness ?? DEFAULT_HARNESS;
|
|
6253
6343
|
const model = opts.model ?? def.model;
|
|
6254
|
-
const
|
|
6255
|
-
|
|
6256
|
-
|
|
6344
|
+
const effort = opts.effort ?? def.effort;
|
|
6345
|
+
const parts = [
|
|
6346
|
+
LAUNCH_MAP[harness],
|
|
6347
|
+
...modelAndEffortArgs(harness, model, effort),
|
|
6348
|
+
...instructionArgs(harness, def.instructions)
|
|
6349
|
+
];
|
|
6350
|
+
const briefInstructions = harness === "cursor" && def.instructions ? def.instructions : void 0;
|
|
6257
6351
|
return {
|
|
6258
6352
|
harness,
|
|
6259
|
-
command: parts.join(" ")
|
|
6353
|
+
command: parts.join(" "),
|
|
6354
|
+
model,
|
|
6355
|
+
effort,
|
|
6356
|
+
...briefInstructions ? { briefInstructions } : {}
|
|
6260
6357
|
};
|
|
6261
6358
|
}
|
|
6262
6359
|
/**
|
|
6263
6360
|
* Resolve what `unit spawn` should launch, from either an explicit `--harness` or an agent def
|
|
6264
|
-
* (`--agent` / `--agent-file`) whose harness, model and instructions compose the launch
|
|
6265
|
-
* An explicit `--harness` overrides the def's own.
|
|
6361
|
+
* (`--agent` / `--agent-file`) whose harness, model, effort and instructions compose the launch
|
|
6362
|
+
* command. An explicit `--harness`/`--model`/`--effort` overrides the def's own, for this launch only.
|
|
6363
|
+
* With no def, a `--model`/`--effort` composes a launch from `--harness` alone; with neither, the
|
|
6364
|
+
* harness's own default command stands (no `command` returned).
|
|
6266
6365
|
*
|
|
6267
6366
|
* Extracted from the CLI action so the def→launch wiring is reachable from a test: composed inline
|
|
6268
6367
|
* it sat between two well-covered halves (`resolveAgentDef`, `realizeLaunch`) with nothing
|
|
6269
6368
|
* exercising the join between them.
|
|
6270
6369
|
*/
|
|
6271
6370
|
function resolveSpawnLaunch(input) {
|
|
6272
|
-
|
|
6273
|
-
|
|
6371
|
+
const overrides = {
|
|
6372
|
+
harness: input.harness,
|
|
6373
|
+
model: input.model,
|
|
6374
|
+
effort: input.effort
|
|
6375
|
+
};
|
|
6376
|
+
if (!input.agent && !input.agentFile) {
|
|
6377
|
+
if (!input.harness || !input.model && !input.effort) return { harness: input.harness };
|
|
6378
|
+
return realizeLaunch({
|
|
6379
|
+
name: input.harness,
|
|
6380
|
+
instructions: "",
|
|
6381
|
+
path: ""
|
|
6382
|
+
}, overrides);
|
|
6383
|
+
}
|
|
6384
|
+
return realizeLaunch(resolveAgentDef({
|
|
6274
6385
|
name: input.agent,
|
|
6275
6386
|
file: input.agentFile,
|
|
6276
6387
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
6277
6388
|
...input.searchRoots ? { searchRoots: input.searchRoots } : {}
|
|
6278
|
-
}),
|
|
6279
|
-
return {
|
|
6280
|
-
harness: realized.harness,
|
|
6281
|
-
command: realized.command
|
|
6282
|
-
};
|
|
6389
|
+
}), overrides);
|
|
6283
6390
|
}
|
|
6284
6391
|
//#endregion
|
|
6285
6392
|
//#region src/cli-input.ts
|
|
6286
6393
|
/**
|
|
6287
6394
|
* Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
|
|
6288
6395
|
* `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
|
|
6289
|
-
* `--harness` overriding the def's own.
|
|
6396
|
+
* `--harness`/`--model`/`--effort` overriding the def's own.
|
|
6290
6397
|
*
|
|
6291
6398
|
* Throws when no harness can be resolved, so the CLI's own `fail()` still renders it.
|
|
6292
6399
|
*/
|
|
6293
6400
|
function spawnCommandInput(opts) {
|
|
6294
|
-
const { harness, command } = resolveSpawnLaunch({
|
|
6401
|
+
const { harness, command, model, effort, briefInstructions } = resolveSpawnLaunch({
|
|
6295
6402
|
agent: opts.agent,
|
|
6296
6403
|
agentFile: opts.agentFile,
|
|
6297
|
-
harness: opts.harness
|
|
6404
|
+
harness: opts.harness,
|
|
6405
|
+
model: opts.model,
|
|
6406
|
+
effort: opts.effort
|
|
6298
6407
|
});
|
|
6299
6408
|
if (!harness) throw new Error("unit spawn needs --harness, or --agent/--agent-file resolving one");
|
|
6300
6409
|
return {
|
|
6301
6410
|
input: {
|
|
6302
6411
|
harness,
|
|
6303
6412
|
command,
|
|
6413
|
+
briefInstructions,
|
|
6304
6414
|
task: opts.task,
|
|
6305
6415
|
briefFile: opts.briefFile,
|
|
6306
6416
|
handle: opts.handle,
|
|
@@ -6309,7 +6419,11 @@ function spawnCommandInput(opts) {
|
|
|
6309
6419
|
cwd: opts.cwd,
|
|
6310
6420
|
at: opts.at
|
|
6311
6421
|
},
|
|
6312
|
-
noWake: opts.wake === false
|
|
6422
|
+
noWake: opts.wake === false,
|
|
6423
|
+
launched: {
|
|
6424
|
+
model,
|
|
6425
|
+
effort
|
|
6426
|
+
}
|
|
6313
6427
|
};
|
|
6314
6428
|
}
|
|
6315
6429
|
/** What `unit read` prints: the raw scrape, or the JSON envelope under `--format json`. */
|
|
@@ -7506,7 +7620,8 @@ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.u
|
|
|
7506
7620
|
function ctxOf(opts) {
|
|
7507
7621
|
return {
|
|
7508
7622
|
store: new FileStore(resolveRoot({ space: opts.space })),
|
|
7509
|
-
env: process.env
|
|
7623
|
+
env: process.env,
|
|
7624
|
+
self: selfInvocation()
|
|
7510
7625
|
};
|
|
7511
7626
|
}
|
|
7512
7627
|
function formatOf(opts) {
|
|
@@ -7692,9 +7807,11 @@ withGlobals(unit.command("prune")).description("mark dead units exited and sweep
|
|
|
7692
7807
|
json: changed
|
|
7693
7808
|
});
|
|
7694
7809
|
});
|
|
7810
|
+
/** What a model/effort/harness field reads when no source set it and the harness's own default applies. */
|
|
7811
|
+
const HARNESS_DEFAULT = "(harness default)";
|
|
7695
7812
|
/** The launch options `unit spawn` and `service start` share. */
|
|
7696
7813
|
function withSpawnOptions(cmd) {
|
|
7697
|
-
return withGlobals(cmd).option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").option("--cwd <path>", "spawn the session in an existing directory; create no worktree (mutually exclusive with --branch/--worktree-path)").addOption(new Option("--at <placement>", "where to open the new session (default: new-worktree → workspace, --cwd → tab)").choices([
|
|
7814
|
+
return withGlobals(cmd).option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/effort/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--model <name>", "model for this launch only (flag > agent def > harness default)").option("--effort <level>", "effort for this launch only (flag > agent def > harness default)").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").option("--cwd <path>", "spawn the session in an existing directory; create no worktree (mutually exclusive with --branch/--worktree-path)").addOption(new Option("--at <placement>", "where to open the new session (default: new-worktree → workspace, --cwd → tab)").choices([
|
|
7698
7815
|
"pane:right",
|
|
7699
7816
|
"pane:down",
|
|
7700
7817
|
"tab",
|
|
@@ -7718,6 +7835,8 @@ function defineSpawn(cmd) {
|
|
|
7718
7835
|
spawned: res.agent.id,
|
|
7719
7836
|
handle: res.agent.handle,
|
|
7720
7837
|
harness: res.agent.harness,
|
|
7838
|
+
model: spawnInput.launched.model ?? HARNESS_DEFAULT,
|
|
7839
|
+
effort: spawnInput.launched.effort ?? HARNESS_DEFAULT,
|
|
7721
7840
|
worktree: res.agent.worktree?.root,
|
|
7722
7841
|
pane: res.pane,
|
|
7723
7842
|
rung: res.rung
|
|
@@ -7726,6 +7845,8 @@ function defineSpawn(cmd) {
|
|
|
7726
7845
|
agent: res.agent,
|
|
7727
7846
|
pane: res.pane,
|
|
7728
7847
|
launch: res.launch,
|
|
7848
|
+
model: spawnInput.launched.model ?? HARNESS_DEFAULT,
|
|
7849
|
+
effort: spawnInput.launched.effort ?? HARNESS_DEFAULT,
|
|
7729
7850
|
rung: res.rung
|
|
7730
7851
|
}
|
|
7731
7852
|
});
|
|
@@ -8207,9 +8328,9 @@ function agentDefFields(d) {
|
|
|
8207
8328
|
return {
|
|
8208
8329
|
name: d.name,
|
|
8209
8330
|
description: d.description,
|
|
8210
|
-
model: d.model ??
|
|
8331
|
+
model: d.model ?? HARNESS_DEFAULT,
|
|
8211
8332
|
effort: d.effort,
|
|
8212
|
-
harness: d.harness ??
|
|
8333
|
+
harness: d.harness ?? HARNESS_DEFAULT,
|
|
8213
8334
|
warm: d.warm ?? false,
|
|
8214
8335
|
interactive: d.interactive ?? false,
|
|
8215
8336
|
path: d.path
|
package/dist/index.d.mts
CHANGED
|
@@ -171,6 +171,10 @@ interface IdContext {
|
|
|
171
171
|
env?: NodeJS.ProcessEnv;
|
|
172
172
|
exec?: Exec$1;
|
|
173
173
|
now?: () => number;
|
|
174
|
+
/** The argv that re-invokes this very CLI — node, its loader flags, and the entry script
|
|
175
|
+
* (`selfInvocation`). `unit spawn` puts a `cyberlegion` shim over it on the new session's PATH,
|
|
176
|
+
* so a unit reports through the install that spawned it. Absent, spawn writes no shim. */
|
|
177
|
+
self?: string[];
|
|
174
178
|
}
|
|
175
179
|
declare function loadAgent(store: Store, id: string): AgentRecord | undefined;
|
|
176
180
|
declare function listAgents(store: Store): AgentRecord[];
|
|
@@ -243,17 +247,25 @@ declare function listAgentDefs(input?: {
|
|
|
243
247
|
interface RealizeLaunchOptions {
|
|
244
248
|
/** Overrides def.model when present. */
|
|
245
249
|
model?: string;
|
|
250
|
+
/** Overrides def.effort when present. */
|
|
251
|
+
effort?: string;
|
|
246
252
|
/** Overrides def.harness when present; falls back to 'claude' when neither is set. */
|
|
247
253
|
harness?: Harness;
|
|
248
254
|
}
|
|
249
255
|
interface RealizedLaunch {
|
|
250
256
|
harness: Harness;
|
|
251
257
|
command: string;
|
|
258
|
+
/** The model and effort the command launches with, from whichever source won; absent when the
|
|
259
|
+
* harness default applies. */
|
|
260
|
+
model?: string;
|
|
261
|
+
effort?: string;
|
|
262
|
+
/** The def's instructions, for a harness whose CLI cannot take them (cursor): `unit spawn` writes
|
|
263
|
+
* them in front of the brief instead. Absent when the command carries them, or there are none. */
|
|
264
|
+
briefInstructions?: string;
|
|
252
265
|
}
|
|
253
|
-
/** Build the harness launch invocation for a def — explicit `model`/`harness` win over the
|
|
254
|
-
* own tags, which win over the harness default.
|
|
255
|
-
*
|
|
256
|
-
* exact per-harness flag surface is realized later by the launching caller, not by cyberlegion. */
|
|
266
|
+
/** Build the harness launch invocation for a def — explicit `model`/`effort`/`harness` win over the
|
|
267
|
+
* def's own tags, which win over the harness default. The def's effort and instructions each go
|
|
268
|
+
* through the harness's own control (see `modelAndEffortArgs`, `instructionArgs`). */
|
|
257
269
|
declare function realizeLaunch(def: AgentDef, opts?: RealizeLaunchOptions): RealizedLaunch;
|
|
258
270
|
//#endregion
|
|
259
271
|
//#region src/decommission.d.ts
|
|
@@ -621,6 +633,9 @@ interface SpawnInput {
|
|
|
621
633
|
* (avoids a session.ts ↔ agentdef/realize.ts import cycle, since `realizeLaunch` already reads
|
|
622
634
|
* `LAUNCH_MAP` from here). */
|
|
623
635
|
command?: string;
|
|
636
|
+
/** A def's instructions its harness cannot take on the command line (an agent-def's
|
|
637
|
+
* `realizeLaunch` output, for cursor): written in front of the brief under their own heading. */
|
|
638
|
+
briefInstructions?: string;
|
|
624
639
|
/** Branch to create the unit's worktree on; defaults to `cyberlegion/unit-<id>`. */
|
|
625
640
|
branch?: string;
|
|
626
641
|
/** Where to check out the unit's worktree; defaults to a sibling of the primary checkout
|