@chllming/wave-orchestration 0.6.1 → 0.6.2

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.
@@ -183,7 +183,7 @@ function findLatestComponentMatches(text) {
183
183
  return Array.from(byComponent.values());
184
184
  }
185
185
 
186
- function detectTermination(logText, statusRecord) {
186
+ function detectTermination(agent, logText, statusRecord) {
187
187
  const patterns = [
188
188
  { reason: "max-turns", regex: /(Reached max turns \(\d+\))/i },
189
189
  { reason: "timeout", regex: /(timed out(?: after [^\n.]+)?)/i },
@@ -192,9 +192,16 @@ function detectTermination(logText, statusRecord) {
192
192
  for (const pattern of patterns) {
193
193
  const match = String(logText || "").match(pattern.regex);
194
194
  if (match) {
195
+ const baseHint = cleanText(match[1] || match[0]);
196
+ if (pattern.reason === "max-turns" && agent?.executorResolved?.id === "codex") {
197
+ return {
198
+ reason: pattern.reason,
199
+ hint: `${baseHint}. Wave does not set a Codex turn-limit flag; inspect launch-preview.json limits for any profile or upstream-runtime ceiling notes.`,
200
+ };
201
+ }
195
202
  return {
196
203
  reason: pattern.reason,
197
- hint: cleanText(match[1] || match[0]),
204
+ hint: baseHint,
198
205
  };
199
206
  }
200
207
  }
@@ -341,7 +348,7 @@ export function buildAgentExecutionSummary({ agent, statusRecord, logPath, repor
341
348
  const reportVerdict = parseVerdictFromText(reportText, REPORT_VERDICT_REGEX);
342
349
  const logVerdict = parseVerdictFromText(signalText, WAVE_VERDICT_REGEX);
343
350
  const verdict = reportVerdict.verdict ? reportVerdict : logVerdict;
344
- const termination = detectTermination(logText, statusRecord);
351
+ const termination = detectTermination(agent, logText, statusRecord);
345
352
  return {
346
353
  agentId: agent?.agentId || null,
347
354
  promptHash: statusRecord?.promptHash || null,
@@ -445,6 +445,19 @@ function normalizeClaudeOutputFormat(value, label = "executors.claude.outputForm
445
445
  return normalized;
446
446
  }
447
447
 
448
+ export function normalizeClaudeEffort(value, label = "executors.claude.effort", fallback = null) {
449
+ const normalized = String(value ?? "")
450
+ .trim()
451
+ .toLowerCase();
452
+ if (!normalized) {
453
+ return fallback;
454
+ }
455
+ if (!["low", "medium", "high", "max"].includes(normalized)) {
456
+ throw new Error(`${label} must be one of: low, medium, high, max`);
457
+ }
458
+ return normalized;
459
+ }
460
+
448
461
  function normalizeOpenCodeFormat(value, label = "executors.opencode.format") {
449
462
  const normalized = String(value || "default")
450
463
  .trim()
@@ -519,6 +532,11 @@ function normalizeExecutorProfile(rawProfile = {}, label = "executors.profiles.<
519
532
  rawProfile.claude.permissionPromptTool,
520
533
  null,
521
534
  ),
535
+ effort: normalizeClaudeEffort(
536
+ rawProfile.claude.effort,
537
+ `${label}.claude.effort`,
538
+ null,
539
+ ),
522
540
  maxTurns: normalizeOptionalPositiveInt(
523
541
  rawProfile.claude.maxTurns,
524
542
  `${label}.claude.maxTurns`,
@@ -685,6 +703,7 @@ function normalizeExecutors(rawExecutors = {}) {
685
703
  executors.claude?.permissionPromptTool,
686
704
  null,
687
705
  ),
706
+ effort: normalizeClaudeEffort(executors.claude?.effort, "executors.claude.effort", null),
688
707
  maxTurns: normalizeOptionalPositiveInt(executors.claude?.maxTurns, "executors.claude.maxTurns"),
689
708
  mcpConfig: normalizeOptionalStringOrStringArray(
690
709
  executors.claude?.mcpConfig,
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { analyzeMessageBoardCommunication } from "./coordination.mjs";
4
- import { commsAgeSummary, deploymentSummary, renderCountsByState } from "./dashboard-state.mjs";
4
+ import { commsAgeSummary, deploymentSummary } from "./dashboard-state.mjs";
5
5
  import {
6
6
  DEFAULT_REFRESH_MS,
7
7
  DEFAULT_WAVE_LANE,
@@ -81,20 +81,125 @@ function isGlobalDashboardState(state) {
81
81
  return Boolean(state && Array.isArray(state.waves) && !Array.isArray(state.agents));
82
82
  }
83
83
 
84
- function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
84
+ const ANSI = {
85
+ reset: "\u001b[0m",
86
+ dim: "\u001b[2m",
87
+ red: "\u001b[31m",
88
+ green: "\u001b[32m",
89
+ yellow: "\u001b[33m",
90
+ blue: "\u001b[34m",
91
+ magenta: "\u001b[35m",
92
+ cyan: "\u001b[36m",
93
+ };
94
+
95
+ function paint(text, color, colorize = false) {
96
+ if (!colorize || !color) {
97
+ return text;
98
+ }
99
+ return `${color}${text}${ANSI.reset}`;
100
+ }
101
+
102
+ function paintState(text, state, colorize = false) {
103
+ const normalized = String(state || "").trim().toLowerCase();
104
+ const color =
105
+ normalized === "completed"
106
+ ? ANSI.green
107
+ : normalized === "failed" || normalized === "timed_out"
108
+ ? ANSI.red
109
+ : normalized === "deploying" || normalized === "finalizing" || normalized === "validating"
110
+ ? ANSI.yellow
111
+ : normalized === "running" || normalized === "coding" || normalized === "launching"
112
+ ? ANSI.cyan
113
+ : normalized === "pending"
114
+ ? ANSI.dim
115
+ : normalized === "dry-run"
116
+ ? ANSI.magenta
117
+ : null;
118
+ return paint(text, color, colorize);
119
+ }
120
+
121
+ function paintLevel(text, level, colorize = false) {
122
+ const normalized = String(level || "").trim().toLowerCase();
123
+ const color =
124
+ normalized === "error"
125
+ ? ANSI.red
126
+ : normalized === "warn"
127
+ ? ANSI.yellow
128
+ : normalized === "info"
129
+ ? ANSI.blue
130
+ : null;
131
+ return paint(text, color, colorize);
132
+ }
133
+
134
+ function paintExitCode(text, exitCode, colorize = false) {
135
+ if (exitCode === 0 || exitCode === "0") {
136
+ return paint(text, ANSI.green, colorize);
137
+ }
138
+ if (exitCode === null || exitCode === undefined || exitCode === "-") {
139
+ return text;
140
+ }
141
+ return paint(text, ANSI.red, colorize);
142
+ }
143
+
144
+ function paintDeploymentSummary(text, deploymentState, colorize = false) {
145
+ const normalized = String(deploymentState || "").trim().toLowerCase();
146
+ const color =
147
+ normalized === "healthy"
148
+ ? ANSI.green
149
+ : normalized === "failed"
150
+ ? ANSI.red
151
+ : normalized === "deploying" || normalized === "rolledover"
152
+ ? ANSI.yellow
153
+ : null;
154
+ return paint(text, color, colorize);
155
+ }
156
+
157
+ function renderColoredCountsByState(agents, colorize = false) {
158
+ const counts = new Map();
159
+ for (const agent of agents || []) {
160
+ const key = agent?.state || "unknown";
161
+ counts.set(key, (counts.get(key) || 0) + 1);
162
+ }
163
+ return Array.from(counts.entries())
164
+ .toSorted((a, b) => a[0].localeCompare(b[0]))
165
+ .map(([state, count]) => `${paintState(state, state, colorize)}:${count}`)
166
+ .join(" ");
167
+ }
168
+
169
+ function paintWaveAgentSummary(summary, wave, colorize = false) {
170
+ const failed = Number(wave?.agentsFailed ?? 0) || 0;
171
+ const active = Number(wave?.agentsActive ?? 0) || 0;
172
+ const total = Number(wave?.agentsTotal ?? 0) || 0;
173
+ const completed = Number(wave?.agentsCompleted ?? 0) || 0;
174
+ const color =
175
+ failed > 0
176
+ ? ANSI.red
177
+ : active > 0
178
+ ? ANSI.cyan
179
+ : total > 0 && completed >= total
180
+ ? ANSI.green
181
+ : ANSI.dim;
182
+ return paint(summary, color, colorize);
183
+ }
184
+
185
+ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane, colorize = false }) {
85
186
  if (!state) {
86
187
  return `Dashboard file not found or invalid: ${dashboardPath}`;
87
188
  }
88
189
  const lines = [];
89
190
  const laneName = String(state?.lane || lane || "").trim() || DEFAULT_WAVE_LANE;
90
191
  lines.push(
91
- `${laneName} Wave Dashboard | wave=${state.wave ?? "?"} | status=${state.status ?? "unknown"} | attempt=${state.attempt ?? "?"}/${state.maxAttempts ?? "?"}`,
192
+ `${laneName} Wave Dashboard | wave=${state.wave ?? "?"} | status=${paintState(
193
+ state.status ?? "unknown",
194
+ state.status,
195
+ colorize,
196
+ )} | attempt=${state.attempt ?? "?"}/${state.maxAttempts ?? "?"}`,
92
197
  );
93
198
  lines.push(
94
199
  `Updated ${formatAgeFromTimestamp(Date.parse(state.updatedAt || ""))} | Started ${state.startedAt || "n/a"} | Elapsed ${formatElapsed(state.startedAt)}`,
95
200
  );
96
201
  lines.push(`Run tag: ${state.runTag || "n/a"} | Wave file: ${state.waveFile || "n/a"}`);
97
- lines.push(`Counts: ${renderCountsByState(state.agents || []) || "none"}`);
202
+ lines.push(`Counts: ${renderColoredCountsByState(state.agents || [], colorize) || "none"}`);
98
203
  const comms = analyzeMessageBoardCommunication(messageBoardPath);
99
204
  if (!comms.available) {
100
205
  lines.push(`Comms: unavailable ${comms.reason || ""}`.trim());
@@ -116,10 +221,19 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
116
221
  );
117
222
  for (const agent of state.agents || []) {
118
223
  lines.push(
119
- `${pad(agent.agentId || "-", 8)} ${pad(agent.state || "-", 12)} ${pad(agent.attempts ?? 0, 8)} ${pad(
120
- agent.exitCode ?? "-",
121
- 6,
122
- )} ${pad(deploymentSummary({ service: agent.deploymentService, state: agent.deploymentState }), 24)} ${pad(
224
+ `${pad(agent.agentId || "-", 8)} ${paintState(
225
+ pad(agent.state || "-", 12),
226
+ agent.state,
227
+ colorize,
228
+ )} ${pad(agent.attempts ?? 0, 8)} ${paintExitCode(
229
+ pad(agent.exitCode ?? "-", 6),
230
+ agent.exitCode,
231
+ colorize,
232
+ )} ${paintDeploymentSummary(
233
+ pad(deploymentSummary({ service: agent.deploymentService, state: agent.deploymentState }), 24),
234
+ agent.deploymentState,
235
+ colorize,
236
+ )} ${pad(
123
237
  formatAgeFromTimestamp(Date.parse(agent.lastUpdateAt || "")),
124
238
  12,
125
239
  )} ${truncate(agent.detail || "", 72)}`,
@@ -130,7 +244,7 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
130
244
  for (const event of (state.events || []).slice(-12)) {
131
245
  const prefix = event.agentId ? `[${event.agentId}]` : "[wave]";
132
246
  lines.push(
133
- `${event.at || "n/a"} ${pad(event.level || "info", 5)} ${prefix} ${event.message || ""}`,
247
+ `${event.at || "n/a"} ${paintLevel(pad(event.level || "info", 5), event.level, colorize)} ${prefix} ${event.message || ""}`,
134
248
  );
135
249
  }
136
250
  if ((state.events || []).length === 0) {
@@ -143,14 +257,29 @@ function renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane }) {
143
257
  return lines.join("\n");
144
258
  }
145
259
 
146
- function renderGlobalDashboard({ state, dashboardPath, lane }) {
260
+ function renderGlobalDashboard({ state, dashboardPath, lane, colorize = false }) {
147
261
  if (!state) {
148
262
  return `Dashboard file not found or invalid: ${dashboardPath}`;
149
263
  }
264
+ const formatGlobalWaveAgentSummary = (wave) => {
265
+ const total = Number(wave?.agentsTotal ?? 0) || 0;
266
+ const completed = Number(wave?.agentsCompleted ?? 0) || 0;
267
+ const failed = Number(wave?.agentsFailed ?? 0) || 0;
268
+ const active = Number(wave?.agentsActive ?? 0) || 0;
269
+ const pending =
270
+ wave?.agentsPending === undefined || wave?.agentsPending === null
271
+ ? Math.max(0, total - completed - failed - active)
272
+ : Number(wave.agentsPending ?? 0) || 0;
273
+ return `done ${completed}/${total} active ${active} pending ${pending} fail ${failed}`;
274
+ };
150
275
  const lines = [];
151
276
  const laneName = String(state?.lane || lane || "").trim() || DEFAULT_WAVE_LANE;
152
277
  lines.push(
153
- `${laneName} Wave Global Dashboard | run=${state.runId || "n/a"} | status=${state.status || "unknown"}`,
278
+ `${laneName} Wave Global Dashboard | run=${state.runId || "n/a"} | status=${paintState(
279
+ state.status || "unknown",
280
+ state.status,
281
+ colorize,
282
+ )}`,
154
283
  );
155
284
  lines.push(
156
285
  `Updated ${formatAgeFromTimestamp(Date.parse(state.updatedAt || ""))} | Started ${state.startedAt || "n/a"} | Elapsed ${formatElapsed(state.startedAt)}`,
@@ -161,18 +290,18 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
161
290
  lines.push("");
162
291
  lines.push("Waves:");
163
292
  lines.push(
164
- `${pad("Wave", 6)} ${pad("Status", 10)} ${pad("Attempt", 9)} ${pad("Agents", 16)} ${pad("Started", 12)} ${pad("Last Message", 70)}`,
293
+ `${pad("Wave", 6)} ${pad("Status", 10)} ${pad("Attempt", 9)} ${pad("Agents", 36)} ${pad("Started", 12)} ${pad("Last Message", 70)}`,
165
294
  );
166
295
  lines.push(
167
- `${"-".repeat(6)} ${"-".repeat(10)} ${"-".repeat(9)} ${"-".repeat(16)} ${"-".repeat(12)} ${"-".repeat(70)}`,
296
+ `${"-".repeat(6)} ${"-".repeat(10)} ${"-".repeat(9)} ${"-".repeat(36)} ${"-".repeat(12)} ${"-".repeat(70)}`,
168
297
  );
169
298
  for (const wave of state.waves || []) {
170
- const agents = `${wave.agentsCompleted ?? 0}/${wave.agentsTotal ?? 0} ok, ${wave.agentsFailed ?? 0} fail`;
299
+ const agents = formatGlobalWaveAgentSummary(wave);
171
300
  lines.push(
172
- `${pad(wave.wave ?? "-", 6)} ${pad(wave.status || "-", 10)} ${pad(
301
+ `${pad(wave.wave ?? "-", 6)} ${paintState(pad(wave.status || "-", 10), wave.status, colorize)} ${pad(
173
302
  `${wave.attempt ?? 0}/${wave.maxAttempts ?? 0}`,
174
303
  9,
175
- )} ${pad(agents, 16)} ${pad(
304
+ )} ${paintWaveAgentSummary(pad(agents, 36), wave, colorize)} ${pad(
176
305
  formatAgeFromTimestamp(Date.parse(wave.startedAt || "")),
177
306
  12,
178
307
  )} ${truncate(wave.lastMessage || "", 70)}`,
@@ -191,7 +320,7 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
191
320
  for (const event of (state.events || []).slice(-16)) {
192
321
  const waveTag = event.wave ? `wave:${event.wave}` : "wave:-";
193
322
  lines.push(
194
- `${event.at || "n/a"} ${pad(event.level || "info", 5)} [${waveTag}] ${event.message || ""}`,
323
+ `${event.at || "n/a"} ${paintLevel(pad(event.level || "info", 5), event.level, colorize)} [${waveTag}] ${event.message || ""}`,
195
324
  );
196
325
  }
197
326
  if ((state.events || []).length === 0) {
@@ -200,10 +329,10 @@ function renderGlobalDashboard({ state, dashboardPath, lane }) {
200
329
  return lines.join("\n");
201
330
  }
202
331
 
203
- export function renderDashboard({ state, dashboardPath, messageBoardPath, lane }) {
332
+ export function renderDashboard({ state, dashboardPath, messageBoardPath, lane, colorize = false }) {
204
333
  return isGlobalDashboardState(state)
205
- ? renderGlobalDashboard({ state, dashboardPath, lane })
206
- : renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane });
334
+ ? renderGlobalDashboard({ state, dashboardPath, lane, colorize })
335
+ : renderWaveDashboard({ state, dashboardPath, messageBoardPath, lane, colorize });
207
336
  }
208
337
 
209
338
  export async function runDashboardCli(argv) {
@@ -232,6 +361,7 @@ Options:
232
361
  dashboardPath: options.dashboardFile,
233
362
  messageBoardPath: boardPath,
234
363
  lane: options.lane,
364
+ colorize: process.stdout.isTTY,
235
365
  });
236
366
  if (process.stdout.isTTY) {
237
367
  process.stdout.write("\u001bc");
@@ -228,8 +228,10 @@ export function buildGlobalDashboardState({
228
228
  startedAt: null,
229
229
  completedAt: null,
230
230
  agentsTotal: wave.agents.length,
231
+ agentsActive: 0,
231
232
  agentsCompleted: 0,
232
233
  agentsFailed: 0,
234
+ agentsPending: wave.agents.length,
233
235
  helperAssignmentsOpen: 0,
234
236
  inboundDependenciesOpen: 0,
235
237
  outboundDependenciesOpen: 0,
@@ -306,10 +308,16 @@ export function syncGlobalWaveFromWaveDashboard(globalState, waveDashboard) {
306
308
  }
307
309
  const agents = Array.isArray(waveDashboard.agents) ? waveDashboard.agents : [];
308
310
  entry.agentsTotal = agents.length;
311
+ entry.agentsActive = agents.filter((agent) =>
312
+ ["launching", "running", "coding", "validating", "deploying", "finalizing"].includes(
313
+ agent.state,
314
+ ),
315
+ ).length;
309
316
  entry.agentsCompleted = agents.filter((agent) => agent.state === "completed").length;
310
317
  entry.agentsFailed = agents.filter(
311
318
  (agent) => agent.state === "failed" || agent.state === "timed_out",
312
319
  ).length;
320
+ entry.agentsPending = agents.filter((agent) => agent.state === "pending").length;
313
321
  const latestEvent = waveDashboard.events.at(-1);
314
322
  entry.lastMessage = latestEvent?.message || entry.lastMessage || "";
315
323
  entry.helperAssignmentsOpen = waveDashboard.helperAssignmentsOpen || 0;
@@ -217,6 +217,7 @@ function buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir }) {
217
217
  appendSingleValueFlag(tokens, "--agent", executor.claude.agent);
218
218
  appendSingleValueFlag(tokens, "--permission-mode", executor.claude.permissionMode);
219
219
  appendSingleValueFlag(tokens, "--permission-prompt-tool", executor.claude.permissionPromptTool);
220
+ appendSingleValueFlag(tokens, "--effort", executor.claude.effort);
220
221
  appendSingleValueFlag(tokens, "--max-turns", executor.claude.maxTurns);
221
222
  appendRepeatedFlag(tokens, "--mcp-config", executor.claude.mcpConfig);
222
223
  appendSingleValueFlag(tokens, "--settings", settingsPath);
@@ -299,6 +300,55 @@ function buildLocalLaunchSpec({ promptPath, logPath }) {
299
300
  };
300
301
  }
301
302
 
303
+ function buildLaunchLimitsMetadata(agent) {
304
+ const executor = agent?.executorResolved || {};
305
+ const executorId = normalizeExecutorMode(executor.id || DEFAULT_EXECUTOR_MODE);
306
+ const attemptTimeoutMinutes = executor?.budget?.minutes ?? null;
307
+ if (executorId === "claude") {
308
+ const source = executor?.claude?.maxTurnsSource || null;
309
+ return {
310
+ attemptTimeoutMinutes,
311
+ knownTurnLimit: executor?.claude?.maxTurns ?? null,
312
+ turnLimitSource: source,
313
+ notes:
314
+ source === "budget.turns"
315
+ ? ["Known turn limit was derived from generic budget.turns."]
316
+ : [],
317
+ };
318
+ }
319
+ if (executorId === "opencode") {
320
+ const source = executor?.opencode?.stepsSource || null;
321
+ return {
322
+ attemptTimeoutMinutes,
323
+ knownTurnLimit: executor?.opencode?.steps ?? null,
324
+ turnLimitSource: source,
325
+ notes:
326
+ source === "budget.turns"
327
+ ? ["Known turn limit was derived from generic budget.turns."]
328
+ : [],
329
+ };
330
+ }
331
+ if (executorId === "codex") {
332
+ const profileNote = executor?.codex?.profileName
333
+ ? ` via Codex profile ${executor.codex.profileName}`
334
+ : "";
335
+ return {
336
+ attemptTimeoutMinutes,
337
+ knownTurnLimit: null,
338
+ turnLimitSource: "not-set-by-wave",
339
+ notes: [
340
+ `Wave emits no Codex turn-limit flag; any effective ceiling may come from the selected Codex profile${profileNote} or the upstream Codex runtime.`,
341
+ ],
342
+ };
343
+ }
344
+ return {
345
+ attemptTimeoutMinutes,
346
+ knownTurnLimit: null,
347
+ turnLimitSource: "not-applicable",
348
+ notes: ["Local executor does not use model turn limits."],
349
+ };
350
+ }
351
+
302
352
  function buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }) {
303
353
  const executor = agent.executorResolved;
304
354
  return {
@@ -329,16 +379,29 @@ function buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }) {
329
379
  export function buildExecutorLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }) {
330
380
  const executorId = normalizeExecutorMode(agent?.executorResolved?.id || DEFAULT_EXECUTOR_MODE);
331
381
  ensureDirectory(overlayDir);
382
+ const limits = buildLaunchLimitsMetadata(agent);
332
383
  if (executorId === "local") {
333
- return buildLocalLaunchSpec({ promptPath, logPath });
384
+ return {
385
+ ...buildLocalLaunchSpec({ promptPath, logPath }),
386
+ limits,
387
+ };
334
388
  }
335
389
  if (executorId === "claude") {
336
- return buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection });
390
+ return {
391
+ ...buildClaudeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
392
+ limits,
393
+ };
337
394
  }
338
395
  if (executorId === "opencode") {
339
- return buildOpenCodeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection });
396
+ return {
397
+ ...buildOpenCodeLaunchSpec({ agent, promptPath, logPath, overlayDir, skillProjection }),
398
+ limits,
399
+ };
340
400
  }
341
- return buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection });
401
+ return {
402
+ ...buildCodexLaunchSpec({ agent, promptPath, logPath, skillProjection }),
403
+ limits,
404
+ };
342
405
  }
343
406
 
344
407
  export function commandForExecutor(executor, executorId = executor?.id) {
@@ -139,6 +139,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
139
139
  env: launchSpec.env || {},
140
140
  useRateLimitRetries: launchSpec.useRateLimitRetries === true,
141
141
  invocationLines: launchSpec.invocationLines,
142
+ limits: launchSpec.limits || null,
142
143
  skills: summarizeResolvedSkills(agent.skillsResolved),
143
144
  });
144
145
  return {