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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberlegion",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberlegion",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
5
5
  "author": {
6
6
  "name": "unional"
package/.plugin/pins.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "cyberlegion": "0.3.1"
2
+ "cyberlegion": "0.5.0"
3
3
  }
@@ -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 { runCli } = await import(join(dir, '..', 'dist', 'cli.mjs'))
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$1(value) {
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$1(value)}`).join(" ")} `;
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 fullLaunch = `${muxEnvPrefix(sessionAdapter.name)}${launch}`;
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: fullLaunch,
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: fullLaunch,
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: fullLaunch,
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
- /** Build the harness launch invocation for a def — explicit `model`/`harness` win over the def's
6248
- * own tags, which win over the harness default. Every harness gets the same shape (bin,
6249
- * `--model` when known, `--append-system-prompt` carrying the def's instructions body) since the
6250
- * exact per-harness flag surface is realized later by the launching caller, not by cyberlegion. */
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 parts = [LAUNCH_MAP[harness]];
6255
- if (model) parts.push("--model", shellQuote(model));
6256
- if (def.instructions) parts.push("--append-system-prompt", shellQuote(def.instructions));
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 command.
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
- if (!input.agent && !input.agentFile) return { harness: input.harness };
6273
- const realized = realizeLaunch(resolveAgentDef({
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
- }), { harness: input.harness });
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 ?? "(harness default)",
8331
+ model: d.model ?? HARNESS_DEFAULT,
8211
8332
  effort: d.effort,
8212
- harness: d.harness ?? "(harness default)",
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 def's
254
- * own tags, which win over the harness default. Every harness gets the same shape (bin,
255
- * `--model` when known, `--append-system-prompt` carrying the def's instructions body) since the
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