@dylanrussell/agent-router 1.0.6 → 1.0.7

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/README.md CHANGED
@@ -29,28 +29,29 @@ opencode agents are markdown files (`~/.config/opencode/agents/*.md`) whose YAML
29
29
  ---
30
30
  description: High-reasoning review, debugging, and architecture counsel
31
31
  mode: subagent
32
- model: anthropic/claude-opus-4-8 # ← the ONLY line agent-router touches
32
+ model: anthropic/claude-opus-4-8 # ← agent-router rewrites this
33
+ reasoningEffort: high # ← and any option keys a stack names
33
34
  temperature: 0.1
34
35
  tools: { write: false, edit: false }
35
36
  ---
36
37
  <prompt body — owned by you, never touched by agent-router>
37
38
  ```
38
39
 
39
- `agent-router` keeps named **stacks** — JSON files mapping agent names to models — and applies them to those `model:` lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
40
+ `agent-router` keeps named **stacks** — JSON files mapping agent names to models (and optionally to provider options) — and applies them to those frontmatter lines on demand. Premium models for the workday, cheap ones for bulk chores, one command to swap the whole crew:
40
41
 
41
42
  ```json
42
43
  {
43
44
  "agents": {
44
45
  "Omni": { "model": "anthropic/claude-fable-5" },
45
- "oracle": { "model": "openai/gpt-5.5" },
46
+ "oracle": { "model": "openai/gpt-5.5", "reasoningEffort": "high" },
46
47
  "explorer": { "model": "openai/gpt-5.4-mini" },
47
48
  "librarian": { "model": "openai/gpt-5.4-mini" },
48
- "fixer": { "model": "openai/gpt-5.5" }
49
+ "fixer": { "model": "openai/gpt-5.5", "thinking": { "effort": "low" } }
49
50
  }
50
51
  }
51
52
  ```
52
53
 
53
- The prompt body and every other frontmatter key are yours; agent-router rewrites exactly one line per agent, atomically, through symlinks (dotfile-manager setups survive intact).
54
+ The prompt body and opencode framework keys (`description`, `mode`, `permission`, `tools`, …) are yours; agent-router rewrites only the `model:` line and the option keys a stack entry names, atomically, through symlinks (dotfile-manager setups survive intact).
54
55
 
55
56
  ```
56
57
  ~/.config/opencode/
@@ -104,11 +105,27 @@ The everyday loop: tune your agents until you like them → `capture <name>` →
104
105
 
105
106
  ## Stacks
106
107
 
107
- A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Unknown keys are preserved round-trip.
108
+ A stack file needs one thing: an `agents` record whose entries carry a `model` string. Agent names are the `.md` basenames in your agents dir (`Omni` ↔ `Omni.md`). Any **other key** on an entry is a provider pass-through option (`reasoningEffort`, `thinking`, `temperature`, `topP`, `maxOutputTokens`, …) that opencode forwards to the model — agent-router transcribes these to the agent's frontmatter alongside `model:` and captures them back. Unknown keys are preserved round-trip.
109
+
110
+ ```json
111
+ {
112
+ "agents": {
113
+ "oracle": {
114
+ "model": "openai/gpt-5.5",
115
+ "reasoningEffort": "high",
116
+ "thinking": { "effort": "low" }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ - **Apply** writes/updates only the option keys a stack entry names; options the stack omits are left in place. To **clear** an option, set it to `null` in the stack entry.
123
+ - Reserved opencode framework keys (`description`, `mode`, `permission`, `tools`, `prompt`, `steps`, `color`, `name`) are never transcribed — a stack entry can't clobber them.
124
+ - Option values serialize to a single frontmatter line: scalars bare (quoted only when needed), objects/arrays as JSON flow. Block-style values an apply replaces are collapsed to flow; `capture` parses them back into typed JSON.
108
125
 
109
126
  `use` is strict by design: if a stack references an agent file that doesn't exist, or one without a `model:` line, the switch fails **before anything is written** — your suite is never left half-switched. It also validates every model ID against `opencode models` first (skip with `--no-validate`, override with `--force-invalid`).
110
127
 
111
- `capture` is the inverse: it reads the current `model:` line of every agent file (files without one are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
128
+ `capture` is the inverse: it reads the current `model:` line and option keys of every agent file (files without a `model:` line are skipped) and writes a stack. There are no bundled seed stacks — your real setup is the seed.
112
129
 
113
130
  ## Inside opencode
114
131
 
@@ -167,12 +184,12 @@ Paths resolve in this order: explicit option → `config.json` → env var → d
167
184
  ## ⚠ Things to know
168
185
 
169
186
  - **Restart required.** opencode reads agent files once at startup. After every `agent-router use`, restart opencode for the new models to take effect. The CLI reminds you.
170
- - **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit.
187
+ - **Hand-edits to frontmatter are not auto-saved into stacks.** If you hand-tune a model or option and want to keep it, `capture` it (or `capture <active> --force`). The next `use` that touches that agent overwrites the hand-edit for keys the stack names; keys the stack omits are preserved (set an option to `null` in a stack entry to clear it).
171
188
  - **Validation is auth-state-dependent.** `agent-router validate` runs `opencode models`, which only lists models reachable through your current auth. If you revoke a key, previously-valid stacks may suddenly be invalid.
172
189
 
173
190
  ## FAQ
174
191
 
175
- **Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it.
192
+ **Why no variants/fallback models?** Native agent frontmatter has a single `model:` line. If you want a fallback, make it a stack (`cheap`, `free`) and switch to it. For per-stack *behavior* variants (reasoning effort, thinking budget, temperature, …), add the option as a sibling key on the stack entry — it transcribes to frontmatter on `use`.
176
193
 
177
194
  **Can I have per-project stacks?** Point `AGENT_ROUTER_STACKS_DIR` at a project-local directory in that project's shell env.
178
195
 
package/dist/cli.js CHANGED
@@ -16086,8 +16086,300 @@ init_errors();
16086
16086
  import { existsSync as existsSync3 } from "fs";
16087
16087
  import { readFile as readFile2, readdir } from "fs/promises";
16088
16088
  import path4 from "path";
16089
+
16090
+ // src/core/yaml-lite.ts
16091
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
16092
+ "null",
16093
+ "true",
16094
+ "false",
16095
+ "yes",
16096
+ "no",
16097
+ "on",
16098
+ "off",
16099
+ "~",
16100
+ // YAML 1.1 nulls
16101
+ "Null",
16102
+ "NULL",
16103
+ "True",
16104
+ "False",
16105
+ "Yes",
16106
+ "No",
16107
+ "On",
16108
+ "Off"
16109
+ ]);
16110
+ var BARE_STRING_RE = /^[A-Za-z0-9._\-\/+@$%^&()~]+$/;
16111
+ function needsQuoting(s) {
16112
+ if (s === "") return true;
16113
+ if (RESERVED_WORDS.has(s)) return true;
16114
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(s)) return true;
16115
+ if (/^0x[0-9A-Fa-f]+$/.test(s)) return true;
16116
+ if (/^[+-]?\.(inf|Inf|INF|nan|NaN|NAN)$/.test(s)) return true;
16117
+ if (!BARE_STRING_RE.test(s)) return true;
16118
+ return false;
16119
+ }
16120
+ function serializeOptionValue(value) {
16121
+ if (value === null || value === void 0) return "null";
16122
+ if (typeof value === "boolean") return value ? "true" : "false";
16123
+ if (typeof value === "number") {
16124
+ if (!Number.isFinite(value)) return String(value);
16125
+ return JSON.stringify(value);
16126
+ }
16127
+ if (typeof value === "bigint") return String(value);
16128
+ if (typeof value === "string") return needsQuoting(value) ? JSON.stringify(value) : value;
16129
+ return JSON.stringify(value);
16130
+ }
16131
+ function stripComment(line) {
16132
+ let inSingle = false;
16133
+ let inDouble = false;
16134
+ for (let i = 0; i < line.length; i++) {
16135
+ const ch = line[i];
16136
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
16137
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
16138
+ else if (ch === "#" && !inSingle && !inDouble) {
16139
+ if (i === 0 || /\s/.test(line[i - 1] ?? "")) return line.slice(0, i);
16140
+ }
16141
+ }
16142
+ return line;
16143
+ }
16144
+ function tokenize(block) {
16145
+ const out = [];
16146
+ for (const raw of block.split(/\r?\n/)) {
16147
+ if (raw.trim() === "") continue;
16148
+ const indentMatch = /^( *)/.exec(raw);
16149
+ const indent = indentMatch?.[1]?.length ?? 0;
16150
+ const content = stripComment(raw.slice(indent));
16151
+ if (content.trim() === "") continue;
16152
+ out.push({ indent, text: content.trimEnd(), raw });
16153
+ }
16154
+ return out;
16155
+ }
16156
+ function coerceScalar(raw) {
16157
+ const v = raw.trim();
16158
+ if (v === "" || v === "null" || v === "~" || v === "Null" || v === "NULL") return null;
16159
+ if (v === "true" || v === "True" || v === "TRUE") return true;
16160
+ if (v === "false" || v === "False" || v === "FALSE") return false;
16161
+ if (v === "yes" || v === "Yes" || v === "YES") return true;
16162
+ if (v === "no" || v === "No" || v === "NO") return false;
16163
+ if (v === "on" || v === "On" || v === "ON") return true;
16164
+ if (v === "off" || v === "Off" || v === "OFF") return false;
16165
+ if (/^[+-]?\d+$/.test(v)) return Number.parseInt(v, 10);
16166
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(v)) return Number.parseFloat(v);
16167
+ if (/^0x[0-9A-Fa-f]+$/.test(v)) return Number.parseInt(v, 16);
16168
+ return v;
16169
+ }
16170
+ function parseQuoted(s) {
16171
+ const quote = s[0] ?? "";
16172
+ if (quote !== '"' && quote !== "'") return null;
16173
+ if (quote === '"') {
16174
+ let out2 = "";
16175
+ for (let i = 1; i < s.length; i++) {
16176
+ const ch = s[i] ?? "";
16177
+ if (ch === "\\" && i + 1 < s.length) {
16178
+ const next = s[i + 1] ?? "";
16179
+ const map2 = {
16180
+ n: "\n",
16181
+ t: " ",
16182
+ r: "\r",
16183
+ '"': '"',
16184
+ "\\": "\\",
16185
+ "/": "/",
16186
+ b: "\b",
16187
+ f: "\f"
16188
+ };
16189
+ out2 += map2[next] ?? next;
16190
+ i++;
16191
+ } else if (ch === '"') {
16192
+ return { value: out2, rest: s.slice(i + 1) };
16193
+ } else {
16194
+ out2 += ch;
16195
+ }
16196
+ }
16197
+ return null;
16198
+ }
16199
+ let out = "";
16200
+ for (let i = 1; i < s.length; i++) {
16201
+ const ch = s[i] ?? "";
16202
+ if (ch === "'") {
16203
+ if (s[i + 1] === "'") {
16204
+ out += "'";
16205
+ i++;
16206
+ } else {
16207
+ return { value: out, rest: s.slice(i + 1) };
16208
+ }
16209
+ } else {
16210
+ out += ch;
16211
+ }
16212
+ }
16213
+ return null;
16214
+ }
16215
+ function splitFlow(raw) {
16216
+ const parts = [];
16217
+ let depth = 0;
16218
+ let inSingle = false;
16219
+ let inDouble = false;
16220
+ let start = 0;
16221
+ for (let i = 0; i < raw.length; i++) {
16222
+ const ch = raw[i];
16223
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
16224
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
16225
+ else if (!inSingle && !inDouble) {
16226
+ if (ch === "{" || ch === "[") depth++;
16227
+ else if (ch === "}" || ch === "]") depth--;
16228
+ else if (ch === "," && depth === 0) {
16229
+ parts.push(raw.slice(start, i));
16230
+ start = i + 1;
16231
+ }
16232
+ }
16233
+ }
16234
+ parts.push(raw.slice(start));
16235
+ return parts;
16236
+ }
16237
+ function parseFlowValue(raw) {
16238
+ const s = raw.trim();
16239
+ if (s === "") return null;
16240
+ const first = s[0] ?? "";
16241
+ if (first === "{") {
16242
+ const inner = s.slice(1, s.length - 1);
16243
+ const obj = {};
16244
+ for (const part of splitFlow(inner)) {
16245
+ const t = part.trim();
16246
+ if (t === "") continue;
16247
+ const colon = findColon(t);
16248
+ if (colon < 0) {
16249
+ obj[t] = null;
16250
+ } else {
16251
+ const k = t.slice(0, colon).trim();
16252
+ const v = t.slice(colon + 1).trim();
16253
+ obj[stripKeyQuotes(k)] = parseFlowValue(v);
16254
+ }
16255
+ }
16256
+ return obj;
16257
+ }
16258
+ if (first === "[") {
16259
+ const inner = s.slice(1, s.length - 1);
16260
+ return splitFlow(inner).filter((p) => p.trim() !== "").map((p) => parseFlowValue(p));
16261
+ }
16262
+ if (first === '"' || first === "'") {
16263
+ const q = parseQuoted(s);
16264
+ return q ? q.value : s;
16265
+ }
16266
+ return coerceScalar(s);
16267
+ }
16268
+ function findColon(s) {
16269
+ let inSingle = false;
16270
+ let inDouble = false;
16271
+ let depth = 0;
16272
+ for (let i = 0; i < s.length; i++) {
16273
+ const ch = s[i];
16274
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
16275
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
16276
+ else if (!inSingle && !inDouble) {
16277
+ if (ch === "{" || ch === "[") depth++;
16278
+ else if (ch === "}" || ch === "]") depth--;
16279
+ else if (ch === ":" && depth === 0) return i;
16280
+ }
16281
+ }
16282
+ return -1;
16283
+ }
16284
+ function stripKeyQuotes(k) {
16285
+ if (k.length >= 2 && (k[0] === '"' && k.at(-1) === '"' || k[0] === "'" && k.at(-1) === "'")) {
16286
+ return k.slice(1, -1);
16287
+ }
16288
+ return k;
16289
+ }
16290
+ function parseMapping(lines, startIdx, indent) {
16291
+ const out = {};
16292
+ let i = startIdx;
16293
+ while (i < lines.length) {
16294
+ const line = lines[i];
16295
+ if (!line) break;
16296
+ if (line.indent < indent) break;
16297
+ if (line.indent > indent) {
16298
+ i++;
16299
+ continue;
16300
+ }
16301
+ const colon = findColon(line.text);
16302
+ if (colon < 0) {
16303
+ i++;
16304
+ continue;
16305
+ }
16306
+ const key = stripKeyQuotes(line.text.slice(0, colon).trim());
16307
+ if (key === "") {
16308
+ i++;
16309
+ continue;
16310
+ }
16311
+ const rest = line.text.slice(colon + 1).trim();
16312
+ if (rest !== "") {
16313
+ out[key] = parseFlowValue(rest);
16314
+ i++;
16315
+ } else {
16316
+ const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
16317
+ let j = i + 1;
16318
+ while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
16319
+ if (j === i + 1) {
16320
+ out[key] = null;
16321
+ i++;
16322
+ } else {
16323
+ const childBlock = lines.slice(i + 1, j);
16324
+ const firstChild = childBlock[0];
16325
+ if (firstChild?.text.trimStart().startsWith("- ")) {
16326
+ out[key] = parseSequence(childBlock, childIndent);
16327
+ } else {
16328
+ out[key] = parseMapping(childBlock, 0, childIndent).value;
16329
+ }
16330
+ i = j;
16331
+ }
16332
+ }
16333
+ }
16334
+ return { value: out, next: i };
16335
+ }
16336
+ function parseSequence(lines, indent) {
16337
+ const out = [];
16338
+ let i = 0;
16339
+ while (i < lines.length) {
16340
+ const line = lines[i];
16341
+ if (!line) break;
16342
+ if (line.indent < indent) break;
16343
+ const t = line.text.trimStart();
16344
+ if (!t.startsWith("- ")) {
16345
+ i++;
16346
+ continue;
16347
+ }
16348
+ const item = t.slice(2).trim();
16349
+ if (item === "") {
16350
+ const childIndent = lines[i + 1]?.indent ?? line.indent + 1;
16351
+ let j = i + 1;
16352
+ while (j < lines.length && (lines[j]?.indent ?? -1) >= childIndent) j++;
16353
+ out.push(parseMapping(lines.slice(i + 1, j), 0, childIndent).value);
16354
+ i = j;
16355
+ } else {
16356
+ out.push(parseFlowValue(item));
16357
+ i++;
16358
+ }
16359
+ }
16360
+ return out;
16361
+ }
16362
+ function parseFrontmatterBlock(block) {
16363
+ const lines = tokenize(block);
16364
+ if (lines.length === 0) return null;
16365
+ const { value } = parseMapping(lines, 0, 0);
16366
+ return value;
16367
+ }
16368
+
16369
+ // src/core/frontmatter.ts
16089
16370
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
16090
16371
  var MODEL_LINE_RE = /^model:[ \t]*(.*)$/m;
16372
+ var RESERVED_AGENT_KEYS = /* @__PURE__ */ new Set([
16373
+ "name",
16374
+ "mode",
16375
+ "description",
16376
+ "permission",
16377
+ "color",
16378
+ "tools",
16379
+ "prompt",
16380
+ "steps",
16381
+ "maxSteps"
16382
+ ]);
16091
16383
  function cleanModelValue(raw) {
16092
16384
  let v = raw;
16093
16385
  const hash2 = v.search(/[ \t]#/);
@@ -16116,6 +16408,55 @@ function setFrontmatterModel(content, model) {
16116
16408
  const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
16117
16409
  return nextFm + content.slice(fm[0].length);
16118
16410
  }
16411
+ function getFrontmatterOptions(content) {
16412
+ const fm = FRONTMATTER_RE.exec(content);
16413
+ if (!fm?.[1]) return {};
16414
+ const parsed = parseFrontmatterBlock(fm[1]);
16415
+ if (!parsed) return {};
16416
+ const out = {};
16417
+ for (const [k, v] of Object.entries(parsed)) {
16418
+ if (k === "model" || RESERVED_AGENT_KEYS.has(k)) continue;
16419
+ out[k] = v;
16420
+ }
16421
+ return out;
16422
+ }
16423
+ function setFrontmatterOptions(content, options) {
16424
+ const fm = FRONTMATTER_RE.exec(content);
16425
+ if (!fm?.[1]) throw new Error("no frontmatter block");
16426
+ const block = fm[1];
16427
+ const keys = Object.keys(options);
16428
+ if (keys.length === 0) return content;
16429
+ const eol = block.includes("\r\n") ? "\r\n" : "\n";
16430
+ const lines = block.split(/\r?\n/);
16431
+ const handled = /* @__PURE__ */ new Set();
16432
+ for (let i = 0; i < lines.length; i++) {
16433
+ const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i] ?? "");
16434
+ if (!m) continue;
16435
+ const key = m[1] ?? "";
16436
+ if (!(key in options)) continue;
16437
+ handled.add(key);
16438
+ let end = i + 1;
16439
+ while (end < lines.length && /^[ \t]+/.test(lines[end] ?? "")) end++;
16440
+ const value = options[key];
16441
+ if (value === null || value === void 0) {
16442
+ lines.splice(i, end - i);
16443
+ i--;
16444
+ } else {
16445
+ lines.splice(i, end - i, `${key}: ${serializeOptionValue(value)}`);
16446
+ }
16447
+ }
16448
+ const appended = [];
16449
+ for (const [key, value] of Object.entries(options)) {
16450
+ if (handled.has(key)) continue;
16451
+ if (value === null || value === void 0) continue;
16452
+ appended.push(`${key}: ${serializeOptionValue(value)}`);
16453
+ }
16454
+ const nextBlock = appended.length > 0 ? lines.join(eol) + eol + appended.join(eol) : lines.join(eol);
16455
+ if (nextBlock === block) return content;
16456
+ const blockStart = fm[0].indexOf(block);
16457
+ const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
16458
+ return nextFm + content.slice(fm[0].length);
16459
+ }
16119
16460
  function agentFilePath(agentsDir, name) {
16120
16461
  return path4.join(agentsDir, `${name}.md`);
16121
16462
  }
@@ -16129,7 +16470,7 @@ async function listAgentFiles(agentsDir) {
16129
16470
  }
16130
16471
  return names.filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -".md".length)).sort();
16131
16472
  }
16132
- async function readAgentModels(agentsDir) {
16473
+ async function readAgentEntries(agentsDir) {
16133
16474
  const out = {};
16134
16475
  for (const name of await listAgentFiles(agentsDir)) {
16135
16476
  const filePath = agentFilePath(agentsDir, name);
@@ -16140,10 +16481,17 @@ async function readAgentModels(agentsDir) {
16140
16481
  throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
16141
16482
  }
16142
16483
  const model = getFrontmatterModel(content);
16143
- if (model !== null) out[name] = model;
16484
+ if (model === null) continue;
16485
+ out[name] = { model, options: getFrontmatterOptions(content) };
16144
16486
  }
16145
16487
  return out;
16146
16488
  }
16489
+ async function readAgentModels(agentsDir) {
16490
+ const entries = await readAgentEntries(agentsDir);
16491
+ const out = {};
16492
+ for (const [name, entry] of Object.entries(entries)) out[name] = entry.model;
16493
+ return out;
16494
+ }
16147
16495
  async function readAgentFileStrict(agentsDir, name) {
16148
16496
  const filePath = agentFilePath(agentsDir, name);
16149
16497
  if (!existsSync3(filePath)) {
@@ -16159,7 +16507,7 @@ async function readAgentFileStrict(agentsDir, name) {
16159
16507
  if (model === null) {
16160
16508
  throw new AgentFileError(name, filePath, "no frontmatter `model:` line to rewrite");
16161
16509
  }
16162
- return { filePath, content, model };
16510
+ return { filePath, content, model, options: getFrontmatterOptions(content) };
16163
16511
  }
16164
16512
 
16165
16513
  // src/core/history.ts
@@ -16428,15 +16776,14 @@ async function applyStack(paths, name, options = {}) {
16428
16776
  const pending = [];
16429
16777
  for (const [agent, entry] of Object.entries(target.agents)) {
16430
16778
  const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
16431
- pending.push({
16432
- agent,
16433
- filePath,
16434
- next: model === entry.model ? null : setFrontmatterModel(content, entry.model)
16435
- });
16779
+ const wantOptions = entryOptions(entry);
16780
+ const afterOptions = setFrontmatterOptions(content, wantOptions);
16781
+ const nextContent = entry.model === model ? afterOptions : setFrontmatterModel(afterOptions, entry.model);
16782
+ pending.push({ agent, filePath, next: nextContent === content ? null : nextContent });
16436
16783
  }
16437
16784
  const prevState = await readState(paths.statePath);
16438
16785
  const prevActive = prevState?.active ?? null;
16439
- const displaced = { agents: modelsToStackAgents(await readAgentModels(paths.agentsDir)) };
16786
+ const displaced = { agents: entriesToStackAgents(await readAgentEntries(paths.agentsDir)) };
16440
16787
  const historyId = await appendHistory(
16441
16788
  paths.historyDir,
16442
16789
  prevActive ?? "(none)",
@@ -16466,11 +16813,24 @@ async function applyStack(paths, name, options = {}) {
16466
16813
  restartRequired: true
16467
16814
  };
16468
16815
  }
16469
- function modelsToStackAgents(models) {
16816
+ function entryOptions(entry) {
16817
+ const rec = entry;
16470
16818
  const out = {};
16471
- for (const k of Object.keys(models).sort()) {
16472
- const model = models[k];
16473
- if (model !== void 0) out[k] = { model };
16819
+ for (const [k, v] of Object.entries(rec)) {
16820
+ if (k === "model" || RESERVED_AGENT_KEYS.has(k)) continue;
16821
+ out[k] = v;
16822
+ }
16823
+ return out;
16824
+ }
16825
+ function entriesToStackAgents(entries) {
16826
+ const out = {};
16827
+ for (const name of Object.keys(entries).sort()) {
16828
+ const entry = entries[name];
16829
+ if (!entry) continue;
16830
+ const { model, options } = entry;
16831
+ const stackEntry = { model };
16832
+ for (const [k, v] of Object.entries(options)) stackEntry[k] = v;
16833
+ out[name] = stackEntry;
16474
16834
  }
16475
16835
  return out;
16476
16836
  }
@@ -16507,8 +16867,8 @@ async function captureStack(paths, name, options = {}) {
16507
16867
  if (existsSync6(dest) && !options.force) {
16508
16868
  throw new UserError(`Stack "${name}" already exists. Use --force to overwrite.`);
16509
16869
  }
16510
- const models = await readAgentModels(paths.agentsDir);
16511
- const agents = modelsToStackAgents(models);
16870
+ const entries = await readAgentEntries(paths.agentsDir);
16871
+ const agents = entriesToStackAgents(entries);
16512
16872
  if (Object.keys(agents).length === 0) {
16513
16873
  throw new UserError(
16514
16874
  `No agent .md files with a frontmatter \`model:\` line found in ${paths.agentsDir}.`
@@ -16551,7 +16911,7 @@ async function exportStack(paths, name, toFile) {
16551
16911
  }
16552
16912
 
16553
16913
  // src/version.ts
16554
- var VERSION = "1.0.6";
16914
+ var VERSION = "1.0.7";
16555
16915
 
16556
16916
  // src/cli.ts
16557
16917
  var log = (...args) => console.log(...args);