iosm-cli 0.2.4 → 0.2.5

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +23 -17
  3. package/dist/core/agent-profiles.d.ts +1 -0
  4. package/dist/core/agent-profiles.d.ts.map +1 -1
  5. package/dist/core/agent-profiles.js +10 -6
  6. package/dist/core/agent-profiles.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +1 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +6 -2
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/agent-teams.d.ts.map +1 -1
  12. package/dist/core/agent-teams.js +90 -19
  13. package/dist/core/agent-teams.js.map +1 -1
  14. package/dist/core/footer-data-provider.d.ts +6 -1
  15. package/dist/core/footer-data-provider.d.ts.map +1 -1
  16. package/dist/core/footer-data-provider.js +9 -0
  17. package/dist/core/footer-data-provider.js.map +1 -1
  18. package/dist/core/parallel-task-agent.d.ts +23 -1
  19. package/dist/core/parallel-task-agent.d.ts.map +1 -1
  20. package/dist/core/parallel-task-agent.js +110 -20
  21. package/dist/core/parallel-task-agent.js.map +1 -1
  22. package/dist/core/shared-memory.d.ts +2 -2
  23. package/dist/core/shared-memory.d.ts.map +1 -1
  24. package/dist/core/shared-memory.js +220 -91
  25. package/dist/core/shared-memory.js.map +1 -1
  26. package/dist/core/singular.d.ts.map +1 -1
  27. package/dist/core/singular.js +3 -1
  28. package/dist/core/singular.js.map +1 -1
  29. package/dist/core/subagents.d.ts +1 -1
  30. package/dist/core/subagents.d.ts.map +1 -1
  31. package/dist/core/subagents.js +11 -3
  32. package/dist/core/subagents.js.map +1 -1
  33. package/dist/core/swarm/planner.d.ts.map +1 -1
  34. package/dist/core/swarm/planner.js +200 -12
  35. package/dist/core/swarm/planner.js.map +1 -1
  36. package/dist/core/swarm/scheduler.d.ts +2 -0
  37. package/dist/core/swarm/scheduler.d.ts.map +1 -1
  38. package/dist/core/swarm/scheduler.js +87 -6
  39. package/dist/core/swarm/scheduler.js.map +1 -1
  40. package/dist/core/system-prompt.d.ts.map +1 -1
  41. package/dist/core/system-prompt.js +1 -0
  42. package/dist/core/system-prompt.js.map +1 -1
  43. package/dist/core/tools/ast-grep.d.ts.map +1 -1
  44. package/dist/core/tools/ast-grep.js +2 -0
  45. package/dist/core/tools/ast-grep.js.map +1 -1
  46. package/dist/core/tools/shared-memory.d.ts.map +1 -1
  47. package/dist/core/tools/shared-memory.js +34 -6
  48. package/dist/core/tools/shared-memory.js.map +1 -1
  49. package/dist/core/tools/task.d.ts.map +1 -1
  50. package/dist/core/tools/task.js +464 -73
  51. package/dist/core/tools/task.js.map +1 -1
  52. package/dist/core/tools/yq.d.ts.map +1 -1
  53. package/dist/core/tools/yq.js +2 -0
  54. package/dist/core/tools/yq.js.map +1 -1
  55. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/footer.js +2 -1
  57. package/dist/modes/interactive/components/footer.js.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  59. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  60. package/dist/modes/interactive/interactive-mode.js +756 -74
  61. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  62. package/docs/cli-reference.md +4 -0
  63. package/docs/interactive-mode.md +2 -0
  64. package/docs/orchestration-and-subagents.md +5 -0
  65. package/package.json +1 -1
@@ -10,7 +10,7 @@ import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown,
10
10
  import { spawn, spawnSync } from "child_process";
11
11
  import { APP_NAME, CHANGELOG_URL, ENV_SESSION_TRACE, ENV_OFFLINE, ENV_SKIP_VERSION_CHECK, getAgentDir, getAuthPath, getDebugLogPath, getModelsPath, getSessionTracePath, getShareViewerUrl, getUpdateInstruction, isSessionTraceEnabled, PACKAGE_NAME, VERSION, } from "../../config.js";
12
12
  import { AuthStorage } from "../../core/auth-storage.js";
13
- import { getAgentProfile, getMainProfileNames, getProfileNames, isValidProfileName, } from "../../core/agent-profiles.js";
13
+ import { getAgentProfile, getMainProfileNames, getProfileNames, isReadOnlyProfileName, isValidProfileName, } from "../../core/agent-profiles.js";
14
14
  import { parseSkillBlock } from "../../core/agent-session.js";
15
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
16
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -88,8 +88,8 @@ function parseRequestedParallelAgentCount(text) {
88
88
  const patterns = [
89
89
  /(\d+)\s+(?:parallel|concurrent)\s+agents?/i,
90
90
  /(\d+)\s+agents?/i,
91
- /(\d+)\s+паралл\w*\s+агент\w*/i,
92
- /(\d+)\s+агент\w*/i,
91
+ /(\d+)\s+паралл[\p{L}\p{N}_-]*\s+агент[\p{L}\p{N}_-]*/iu,
92
+ /(\d+)\s+агент[\p{L}\p{N}_-]*/iu,
93
93
  ];
94
94
  for (const pattern of patterns) {
95
95
  const match = text.match(pattern);
@@ -127,6 +127,9 @@ function buildMetaParallelismCorrection(input) {
127
127
  `Meta runtime correction: this run currently has ${input.launchedTopLevelTasks} top-level task call(s), but it should have at least ${input.requiredTopLevelTasks}.`,
128
128
  "Stop manual sequential execution in the main agent and convert the execution graph into parallel task calls now.",
129
129
  "Emit the missing top-level task calls in the same assistant response when branches are independent.",
130
+ input.workerDiversityMissing
131
+ ? `Top-level task fan-out currently targets only ${input.distinctWorkers ?? 1} worker identity. Use at least 2 focused worker identities (profile/agent) or force nested delegation inside each stream.`
132
+ : undefined,
130
133
  "Each task tool call MUST include description and prompt.",
131
134
  'If you want a custom subagent, pass it via agent="name"; keep profile set to the capability profile, not the custom subagent name.',
132
135
  input.rawRootDelegateBlocks && input.rawRootDelegateBlocks > 0
@@ -151,19 +154,29 @@ async function promptMetaWithParallelismGuard(input) {
151
154
  let rawRootDelegateBlocks = 0;
152
155
  let delegationImpossibleDeclared = false;
153
156
  let correctionText;
154
- const maybeScheduleCorrection = () => {
157
+ let distinctTaskWorkers = new Set();
158
+ let delegatedChildTasksSeen = 0;
159
+ const maybeScheduleCorrection = (options) => {
155
160
  const requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
156
- if (!requiredTopLevelTasks || taskToolCalls >= requiredTopLevelTasks || delegationImpossibleDeclared) {
161
+ if (!requiredTopLevelTasks || delegationImpossibleDeclared) {
162
+ return;
163
+ }
164
+ const needsMoreTopLevelTasks = taskToolCalls < requiredTopLevelTasks;
165
+ const workerDiversityMissing = !needsMoreTopLevelTasks &&
166
+ requiredTopLevelTasks >= 2 &&
167
+ distinctTaskWorkers.size === 1 &&
168
+ delegatedChildTasksSeen === 0;
169
+ if (!needsMoreTopLevelTasks && !workerDiversityMissing) {
157
170
  return;
158
171
  }
159
172
  const hasComplexPlan = !!taskPlanSnapshot;
160
173
  const hasTaskFailure = !!taskToolError;
161
174
  const hasRawRootDelegates = rawRootDelegateBlocks > 0;
162
- if (!hasTaskFailure && !hasRawRootDelegates && nonTaskToolCalls < 1) {
163
- return;
164
- }
165
- if (!hasComplexPlan && !hasTaskFailure && !hasRawRootDelegates && nonTaskToolCalls < 2) {
166
- return;
175
+ if (needsMoreTopLevelTasks && !hasTaskFailure && !hasRawRootDelegates) {
176
+ const minimumNonTaskCalls = hasComplexPlan ? 0 : 2;
177
+ if (!options?.finalize && nonTaskToolCalls < minimumNonTaskCalls) {
178
+ return;
179
+ }
167
180
  }
168
181
  correctionText = buildMetaParallelismCorrection({
169
182
  requiredTopLevelTasks,
@@ -171,6 +184,8 @@ async function promptMetaWithParallelismGuard(input) {
171
184
  taskPlanSnapshot,
172
185
  taskToolError,
173
186
  rawRootDelegateBlocks,
187
+ workerDiversityMissing,
188
+ distinctWorkers: distinctTaskWorkers.size,
174
189
  });
175
190
  };
176
191
  const unsubscribe = input.session.subscribe((event) => {
@@ -192,6 +207,17 @@ async function promptMetaWithParallelismGuard(input) {
192
207
  if (event.type === "tool_execution_start") {
193
208
  if (event.toolName === "task") {
194
209
  taskToolCalls += 1;
210
+ const args = event.args && typeof event.args === "object" ? event.args : undefined;
211
+ const workerIdentityRaw = (typeof args?.agent === "string" && args.agent.trim()) ||
212
+ (typeof args?.profile === "string" && args.profile.trim()) ||
213
+ undefined;
214
+ if (workerIdentityRaw) {
215
+ distinctTaskWorkers.add(workerIdentityRaw.toLowerCase());
216
+ }
217
+ else if (args) {
218
+ // Distinguish "explicit task call with omitted identity" from missing event payload.
219
+ distinctTaskWorkers.add("__default_profile__");
220
+ }
195
221
  }
196
222
  else {
197
223
  nonTaskToolCalls += 1;
@@ -202,20 +228,54 @@ async function promptMetaWithParallelismGuard(input) {
202
228
  if (event.type === "tool_execution_end" && event.toolName === "task" && event.isError) {
203
229
  taskToolError = extractTaskToolErrorText(event.result) ?? taskToolError;
204
230
  maybeScheduleCorrection();
231
+ return;
232
+ }
233
+ if (event.type === "tool_execution_end" && event.toolName === "task" && !event.isError) {
234
+ const result = event.result;
235
+ const delegatedTasks = result?.details?.delegatedTasks;
236
+ if (typeof delegatedTasks === "number" && Number.isFinite(delegatedTasks) && delegatedTasks > 0) {
237
+ delegatedChildTasksSeen += delegatedTasks;
238
+ }
239
+ maybeScheduleCorrection();
205
240
  }
206
241
  });
207
242
  try {
208
243
  await input.session.prompt(input.userInput);
209
- const requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
244
+ maybeScheduleCorrection({ finalize: true });
245
+ let requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
246
+ let workerDiversityMissingAfterRun = !!requiredTopLevelTasks &&
247
+ requiredTopLevelTasks >= 2 &&
248
+ distinctTaskWorkers.size === 1 &&
249
+ delegatedChildTasksSeen === 0;
210
250
  if (correctionText &&
211
251
  requiredTopLevelTasks &&
212
- taskToolCalls < requiredTopLevelTasks &&
252
+ (taskToolCalls < requiredTopLevelTasks || workerDiversityMissingAfterRun) &&
213
253
  !delegationImpossibleDeclared) {
214
254
  await input.session.prompt(correctionText, {
215
255
  expandPromptTemplates: false,
216
256
  skipOrchestrationDirective: true,
217
257
  source: "interactive",
218
258
  });
259
+ maybeScheduleCorrection({ finalize: true });
260
+ requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
261
+ workerDiversityMissingAfterRun =
262
+ !!requiredTopLevelTasks &&
263
+ requiredTopLevelTasks >= 2 &&
264
+ distinctTaskWorkers.size === 1 &&
265
+ delegatedChildTasksSeen === 0;
266
+ }
267
+ if (requiredTopLevelTasks &&
268
+ (taskToolCalls < requiredTopLevelTasks || workerDiversityMissingAfterRun) &&
269
+ !delegationImpossibleDeclared &&
270
+ input.onPersistentNonCompliance) {
271
+ await input.onPersistentNonCompliance({
272
+ requiredTopLevelTasks,
273
+ launchedTopLevelTasks: taskToolCalls,
274
+ distinctWorkers: distinctTaskWorkers.size,
275
+ workerDiversityMissing: workerDiversityMissingAfterRun,
276
+ taskPlanSnapshot,
277
+ taskToolError,
278
+ });
219
279
  }
220
280
  }
221
281
  finally {
@@ -549,6 +609,8 @@ export class InteractiveMode {
549
609
  this.sessionAllowedToolSignatures = new Set();
550
610
  this.singularLastEffectiveContract = {};
551
611
  this.swarmActiveRunId = undefined;
612
+ this.swarmStopRequested = false;
613
+ this.swarmAbortController = undefined;
552
614
  this.lastSigintTime = 0;
553
615
  this.lastEscapeTime = 0;
554
616
  this.changelogMarkdown = undefined;
@@ -559,6 +621,7 @@ export class InteractiveMode {
559
621
  this.streamingComponent = undefined;
560
622
  this.streamingMessage = undefined;
561
623
  this.currentTurnSawAssistantMessage = false;
624
+ this.currentTurnSawTaskToolCall = false;
562
625
  // Tool execution tracking: toolCallId -> component
563
626
  this.pendingTools = new Map();
564
627
  // Subagent execution tracking with live progress/metadata for task tool calls.
@@ -737,6 +800,17 @@ export class InteractiveMode {
737
800
  }
738
801
  }
739
802
  async requestToolPermission(request) {
803
+ if (this.activeProfileName === "meta" &&
804
+ !this.currentTurnSawTaskToolCall &&
805
+ (request.toolName === "bash" || request.toolName === "edit" || request.toolName === "write")) {
806
+ this.showWarning(`META mode orchestration guard: direct ${request.toolName} is blocked before the first task call in a turn. Launch subagents via task or switch profile to full.`);
807
+ return false;
808
+ }
809
+ if (isReadOnlyProfileName(this.activeProfileName) &&
810
+ (request.toolName === "bash" || request.toolName === "edit" || request.toolName === "write")) {
811
+ this.showWarning(`Tool ${request.toolName} is disabled in ${this.activeProfileName} profile. Switch to full/meta/iosm for mutating operations.`);
812
+ return false;
813
+ }
740
814
  for (const rule of this.permissionDenyRules) {
741
815
  if (this.matchesPermissionRule(rule, request)) {
742
816
  this.showWarning(`Denied by rule: ${rule}`);
@@ -2138,7 +2212,7 @@ export class InteractiveMode {
2138
2212
  sessionManager: this.sessionManager,
2139
2213
  modelRegistry: this.session.modelRegistry,
2140
2214
  model: this.session.model,
2141
- isIdle: () => !this.session.isStreaming,
2215
+ isIdle: () => !this.session.isStreaming && this.swarmActiveRunId === undefined,
2142
2216
  abort: () => this.session.abort(),
2143
2217
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
2144
2218
  shutdown: () => {
@@ -2710,6 +2784,7 @@ export class InteractiveMode {
2710
2784
  this.session.isRetrying ||
2711
2785
  this.iosmAutomationRun !== undefined ||
2712
2786
  this.iosmVerificationSession !== undefined ||
2787
+ this.swarmActiveRunId !== undefined ||
2713
2788
  this.singularAnalysisSession !== undefined ||
2714
2789
  queuedMessages.steering.length > 0 ||
2715
2790
  queuedMessages.followUp.length > 0 ||
@@ -3116,6 +3191,173 @@ export class InteractiveMode {
3116
3191
  durationMs: Date.now() - subagent.startTime,
3117
3192
  });
3118
3193
  }
3194
+ getSwarmSubagentKey(runId, taskId) {
3195
+ return `swarm:${runId}:${taskId}`;
3196
+ }
3197
+ ensureSwarmSubagentDisplay(input) {
3198
+ const key = this.getSwarmSubagentKey(input.runId, input.taskId);
3199
+ const existing = this.subagentComponents.get(key);
3200
+ if (existing) {
3201
+ return existing;
3202
+ }
3203
+ const profile = (input.profile?.trim() || this.resolveSwarmTaskProfile(input.task)).trim();
3204
+ const info = {
3205
+ description: input.task.brief || input.task.id,
3206
+ profile,
3207
+ status: "running",
3208
+ phase: "starting subagent",
3209
+ phaseState: "starting",
3210
+ cwd: this.sessionManager.getCwd(),
3211
+ toolCallsStarted: 0,
3212
+ toolCallsCompleted: 0,
3213
+ assistantMessages: 0,
3214
+ delegatedTasks: 0,
3215
+ delegatedSucceeded: 0,
3216
+ delegatedFailed: 0,
3217
+ };
3218
+ const component = new SubagentMessageComponent(info);
3219
+ this.chatContainer.addChild(component);
3220
+ const state = {
3221
+ component,
3222
+ startTime: Date.now(),
3223
+ profile: info.profile,
3224
+ description: info.description,
3225
+ cwd: info.cwd,
3226
+ agent: info.agent,
3227
+ lockKey: info.lockKey,
3228
+ isolation: info.isolation,
3229
+ phase: info.phase,
3230
+ phaseState: info.phaseState,
3231
+ activeTool: info.activeTool,
3232
+ toolCallsStarted: info.toolCallsStarted ?? 0,
3233
+ toolCallsCompleted: info.toolCallsCompleted ?? 0,
3234
+ assistantMessages: info.assistantMessages ?? 0,
3235
+ delegatedTasks: info.delegatedTasks,
3236
+ delegatedSucceeded: info.delegatedSucceeded,
3237
+ delegatedFailed: info.delegatedFailed,
3238
+ delegateIndex: info.delegateIndex,
3239
+ delegateTotal: info.delegateTotal,
3240
+ delegateDescription: info.delegateDescription,
3241
+ delegateProfile: info.delegateProfile,
3242
+ delegateItems: info.delegateItems,
3243
+ };
3244
+ this.subagentComponents.set(key, state);
3245
+ this.ensureSubagentElapsedTimer();
3246
+ this.ui.requestRender();
3247
+ return state;
3248
+ }
3249
+ updateSwarmSubagentProgress(input) {
3250
+ const state = this.ensureSwarmSubagentDisplay({
3251
+ runId: input.runId,
3252
+ taskId: input.taskId,
3253
+ task: input.task,
3254
+ profile: input.profile,
3255
+ });
3256
+ const progress = input.progress;
3257
+ if (typeof progress.phase === "string" && progress.phase.trim()) {
3258
+ state.phase = progress.phase.trim();
3259
+ }
3260
+ if (isSubagentPhaseState(progress.phaseState)) {
3261
+ state.phaseState = progress.phaseState;
3262
+ }
3263
+ if (typeof progress.cwd === "string" && progress.cwd.trim()) {
3264
+ state.cwd = progress.cwd.trim();
3265
+ }
3266
+ if ("activeTool" in progress) {
3267
+ state.activeTool =
3268
+ typeof progress.activeTool === "string" && progress.activeTool.trim().length > 0
3269
+ ? progress.activeTool.trim()
3270
+ : undefined;
3271
+ }
3272
+ if (typeof progress.toolCallsStarted === "number" && Number.isFinite(progress.toolCallsStarted)) {
3273
+ state.toolCallsStarted = Math.max(0, progress.toolCallsStarted);
3274
+ }
3275
+ if (typeof progress.toolCallsCompleted === "number" && Number.isFinite(progress.toolCallsCompleted)) {
3276
+ state.toolCallsCompleted = Math.max(0, progress.toolCallsCompleted);
3277
+ }
3278
+ if (typeof progress.assistantMessages === "number" && Number.isFinite(progress.assistantMessages)) {
3279
+ state.assistantMessages = Math.max(0, progress.assistantMessages);
3280
+ }
3281
+ if (typeof progress.delegatedTasks === "number" && Number.isFinite(progress.delegatedTasks)) {
3282
+ state.delegatedTasks = Math.max(0, progress.delegatedTasks);
3283
+ }
3284
+ if (typeof progress.delegatedSucceeded === "number" && Number.isFinite(progress.delegatedSucceeded)) {
3285
+ state.delegatedSucceeded = Math.max(0, progress.delegatedSucceeded);
3286
+ }
3287
+ if (typeof progress.delegatedFailed === "number" && Number.isFinite(progress.delegatedFailed)) {
3288
+ state.delegatedFailed = Math.max(0, progress.delegatedFailed);
3289
+ }
3290
+ if ("delegateIndex" in progress) {
3291
+ state.delegateIndex =
3292
+ typeof progress.delegateIndex === "number" && progress.delegateIndex > 0
3293
+ ? Math.floor(progress.delegateIndex)
3294
+ : undefined;
3295
+ }
3296
+ if ("delegateTotal" in progress) {
3297
+ state.delegateTotal =
3298
+ typeof progress.delegateTotal === "number" && progress.delegateTotal > 0
3299
+ ? Math.floor(progress.delegateTotal)
3300
+ : undefined;
3301
+ }
3302
+ if ("delegateDescription" in progress) {
3303
+ state.delegateDescription =
3304
+ typeof progress.delegateDescription === "string" && progress.delegateDescription.trim().length > 0
3305
+ ? progress.delegateDescription.trim()
3306
+ : undefined;
3307
+ }
3308
+ if ("delegateProfile" in progress) {
3309
+ state.delegateProfile =
3310
+ typeof progress.delegateProfile === "string" && progress.delegateProfile.trim().length > 0
3311
+ ? progress.delegateProfile.trim()
3312
+ : undefined;
3313
+ }
3314
+ if ("delegateItems" in progress) {
3315
+ state.delegateItems = Array.isArray(progress.delegateItems) ? progress.delegateItems : undefined;
3316
+ }
3317
+ this.updateRunningSubagentDisplay(state);
3318
+ this.ui.requestRender();
3319
+ }
3320
+ finalizeSwarmSubagentDisplay(input) {
3321
+ const key = this.getSwarmSubagentKey(input.runId, input.taskId);
3322
+ const state = this.subagentComponents.get(key);
3323
+ if (!state)
3324
+ return;
3325
+ const durationMs = Date.now() - state.startTime;
3326
+ state.component.update({
3327
+ description: state.description,
3328
+ profile: state.profile,
3329
+ status: input.status,
3330
+ durationMs,
3331
+ phaseState: state.phaseState,
3332
+ cwd: state.cwd,
3333
+ agent: state.agent,
3334
+ lockKey: state.lockKey,
3335
+ isolation: state.isolation,
3336
+ toolCallsStarted: state.toolCallsStarted,
3337
+ toolCallsCompleted: state.toolCallsCompleted,
3338
+ assistantMessages: state.assistantMessages,
3339
+ delegatedTasks: state.delegatedTasks,
3340
+ delegatedSucceeded: state.delegatedSucceeded,
3341
+ delegatedFailed: state.delegatedFailed,
3342
+ errorMessage: input.status === "error" ? input.errorMessage ?? "error" : undefined,
3343
+ });
3344
+ this.subagentComponents.delete(key);
3345
+ this.stopSubagentElapsedTimerIfIdle();
3346
+ this.ui.requestRender();
3347
+ }
3348
+ finalizeSwarmRunSubagentDisplays(runId, errorMessage) {
3349
+ for (const [key] of this.subagentComponents.entries()) {
3350
+ if (!key.startsWith(`swarm:${runId}:`))
3351
+ continue;
3352
+ const taskId = key.slice(`swarm:${runId}:`.length);
3353
+ this.finalizeSwarmSubagentDisplay({
3354
+ runId,
3355
+ taskId,
3356
+ status: "error",
3357
+ errorMessage,
3358
+ });
3359
+ }
3360
+ }
3119
3361
  ensureSubagentElapsedTimer() {
3120
3362
  if (this.subagentElapsedTimer || this.subagentComponents.size === 0) {
3121
3363
  return;
@@ -3156,6 +3398,7 @@ export class InteractiveMode {
3156
3398
  switch (event.type) {
3157
3399
  case "agent_start":
3158
3400
  this.currentTurnSawAssistantMessage = false;
3401
+ this.currentTurnSawTaskToolCall = false;
3159
3402
  // Restore main escape handler if retry handler is still active
3160
3403
  // (retry success event fires later, but we need main handler now)
3161
3404
  if (this.retryEscapeHandler) {
@@ -3286,6 +3529,9 @@ export class InteractiveMode {
3286
3529
  this.ui.requestRender();
3287
3530
  break;
3288
3531
  case "tool_execution_start": {
3532
+ if (event.toolName === "task") {
3533
+ this.currentTurnSawTaskToolCall = true;
3534
+ }
3289
3535
  if (event.toolName === "task" && !this.subagentComponents.has(event.toolCallId)) {
3290
3536
  const staleTaskComponent = this.pendingTools.get(event.toolCallId);
3291
3537
  if (staleTaskComponent) {
@@ -3922,6 +4168,15 @@ export class InteractiveMode {
3922
4168
  // =========================================================================
3923
4169
  handleCtrlC() {
3924
4170
  const now = Date.now();
4171
+ if (this.swarmActiveRunId) {
4172
+ if (this.swarmStopRequested && now - this.lastSigintTime < 500) {
4173
+ void this.shutdown();
4174
+ return;
4175
+ }
4176
+ this.lastSigintTime = now;
4177
+ void this.interruptCurrentWork();
4178
+ return;
4179
+ }
3925
4180
  if (this.iosmAutomationRun) {
3926
4181
  if (this.iosmAutomationRun.cancelRequested && now - this.lastSigintTime < 500) {
3927
4182
  void this.shutdown();
@@ -4329,6 +4584,7 @@ export class InteractiveMode {
4329
4584
  const hasAutomationWork = this.iosmAutomationRun !== undefined;
4330
4585
  const verificationSession = this.iosmVerificationSession;
4331
4586
  const hasVerificationWork = verificationSession !== undefined;
4587
+ const hasSwarmWork = this.swarmActiveRunId !== undefined;
4332
4588
  const singularSession = this.singularAnalysisSession;
4333
4589
  const hasSingularWork = singularSession !== undefined;
4334
4590
  const hasMainStreaming = this.session.isStreaming;
@@ -4338,6 +4594,7 @@ export class InteractiveMode {
4338
4594
  if (!hasPendingQueuedMessages &&
4339
4595
  !hasAutomationWork &&
4340
4596
  !hasVerificationWork &&
4597
+ !hasSwarmWork &&
4341
4598
  !hasSingularWork &&
4342
4599
  !hasMainStreaming &&
4343
4600
  !hasRetryWork &&
@@ -4351,6 +4608,10 @@ export class InteractiveMode {
4351
4608
  if (this.iosmAutomationRun) {
4352
4609
  this.iosmAutomationRun.cancelRequested = true;
4353
4610
  }
4611
+ if (hasSwarmWork) {
4612
+ this.swarmStopRequested = true;
4613
+ this.swarmAbortController?.abort();
4614
+ }
4354
4615
  if (hasPendingQueuedMessages) {
4355
4616
  this.restoreQueuedMessagesToEditor();
4356
4617
  }
@@ -4371,9 +4632,11 @@ export class InteractiveMode {
4371
4632
  ? "Stopping IOSM automation..."
4372
4633
  : hasVerificationWork
4373
4634
  ? "Stopping IOSM verification..."
4374
- : hasSingularWork
4375
- ? "Stopping /singular analysis..."
4376
- : "Stopping current run...");
4635
+ : hasSwarmWork
4636
+ ? "Stopping swarm run..."
4637
+ : hasSingularWork
4638
+ ? "Stopping /singular analysis..."
4639
+ : "Stopping current run...");
4377
4640
  const abortPromises = [];
4378
4641
  if (hasMainStreaming) {
4379
4642
  abortPromises.push(this.session.abort());
@@ -7866,8 +8129,10 @@ export class InteractiveMode {
7866
8129
  });
7867
8130
  options.push("Close without decision");
7868
8131
  const selected = await this.showExtensionSelector("/singular: choose next step", options);
7869
- if (!selected || selected === "Close without decision")
8132
+ if (!selected || selected === "Close without decision") {
8133
+ this.showStatus("Singular: decision closed without execution.");
7870
8134
  return;
8135
+ }
7871
8136
  const match = selected.match(/^Option\s+(\d+)/);
7872
8137
  if (!match)
7873
8138
  return;
@@ -8879,6 +9144,20 @@ export class InteractiveMode {
8879
9144
  await promptMetaWithParallelismGuard({
8880
9145
  session: this.session,
8881
9146
  userInput,
9147
+ onPersistentNonCompliance: async (details) => {
9148
+ if (typeof this.runSwarmFromTask !== "function")
9149
+ return;
9150
+ if (this.session.isStreaming || this.iosmAutomationRun || this.iosmVerificationSession)
9151
+ return;
9152
+ const explicitRequested = parseRequestedParallelAgentCount(userInput);
9153
+ const hasComplexSignal = /\b(audit|security|hardening|refactor|migration|orchestrat|parallel|delegate|multi[-\s]?agent)\b/i.test(userInput);
9154
+ const fallbackParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, explicitRequested ??
9155
+ (hasComplexSignal
9156
+ ? Math.max(details.requiredTopLevelTasks, 6)
9157
+ : Math.max(details.requiredTopLevelTasks, 3))));
9158
+ this.showWarning(`META enforcement fallback: orchestration contract not satisfied (${details.launchedTopLevelTasks}/${details.requiredTopLevelTasks} task calls). Launching /swarm run.`);
9159
+ await this.runSwarmFromTask(userInput, { maxParallel: fallbackParallel });
9160
+ },
8882
9161
  });
8883
9162
  return;
8884
9163
  }
@@ -9116,6 +9395,40 @@ export class InteractiveMode {
9116
9395
  command: commandParts.join(" "),
9117
9396
  };
9118
9397
  }
9398
+ resolveOrchestrateDefaultAssignmentProfile(parsed) {
9399
+ const active = this.activeProfileName || "full";
9400
+ if (parsed.mode !== "parallel")
9401
+ return active;
9402
+ if (parsed.profile || (parsed.profiles && parsed.profiles.length > 0))
9403
+ return active;
9404
+ if (isReadOnlyProfileName(active))
9405
+ return active;
9406
+ return "meta";
9407
+ }
9408
+ deriveOrchestrateDelegateParallelHint(input) {
9409
+ const normalizedTask = input.task.toLowerCase();
9410
+ const highRisk = /\b(refactor|rewrite|migration|migrate|breaking|rollback|security|auth|authentication|authorization|permission|payment|billing|schema|database|critical)\b/i.test(normalizedTask);
9411
+ const mediumRisk = /\b(cross[-\s]?module|architecture|infra|platform|multi[-\s]?file|integration|audit|hardening)\b/i.test(normalizedTask);
9412
+ let hint = input.mode === "parallel"
9413
+ ? Math.max(2, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, input.maxParallel ?? input.agents))
9414
+ : 1;
9415
+ if (highRisk) {
9416
+ hint = Math.max(hint, 7);
9417
+ }
9418
+ else if (mediumRisk) {
9419
+ hint = Math.max(hint, 5);
9420
+ }
9421
+ if (input.mode === "parallel" && input.dependencyEdges >= input.agents) {
9422
+ hint = Math.max(hint, 6);
9423
+ }
9424
+ if (input.hasDependencies) {
9425
+ hint = Math.max(2, Math.min(hint, 6));
9426
+ }
9427
+ if (input.hasLock) {
9428
+ hint = Math.max(1, Math.min(hint, 4));
9429
+ }
9430
+ return Math.max(1, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, hint));
9431
+ }
9119
9432
  isEffectiveContractReady(contract) {
9120
9433
  const hasText = (value) => typeof value === "string" && value.trim().length > 0;
9121
9434
  const hasList = (value) => Array.isArray(value) && value.some((item) => item.trim().length > 0);
@@ -9452,12 +9765,15 @@ export class InteractiveMode {
9452
9765
  };
9453
9766
  }
9454
9767
  resolveSwarmTaskProfile(task) {
9768
+ const hint = this.deriveSwarmTaskDelegateParallelHint(task);
9769
+ if (task.concurrency_class === "docs")
9770
+ return "plan";
9771
+ if (hint >= 2)
9772
+ return "meta";
9455
9773
  if (task.concurrency_class === "analysis")
9456
9774
  return "explore";
9457
9775
  if (task.concurrency_class === "verification" || task.concurrency_class === "tests")
9458
9776
  return "iosm_verifier";
9459
- if (task.concurrency_class === "docs")
9460
- return "plan";
9461
9777
  return "full";
9462
9778
  }
9463
9779
  estimateSwarmTaskCostUsd(task) {
@@ -9473,6 +9789,13 @@ export class InteractiveMode {
9473
9789
  const hasVeryComplexSignal = /(overhaul|major|system-wide|cross-cutting|multi-service|facet|registry)/i.test(brief) ||
9474
9790
  task.touches.length >= 5 ||
9475
9791
  task.scopes.length >= 4;
9792
+ if (task.concurrency_class === "analysis" || task.concurrency_class === "docs") {
9793
+ if (task.severity === "low")
9794
+ return 1;
9795
+ if (task.touches.length >= 6 || task.scopes.length >= 4 || hasComplexKeyword)
9796
+ return 2;
9797
+ return 1;
9798
+ }
9476
9799
  if (task.severity === "low" && task.touches.length <= 2 && task.scopes.length <= 2 && !hasComplexKeyword) {
9477
9800
  return 1;
9478
9801
  }
@@ -9487,6 +9810,38 @@ export class InteractiveMode {
9487
9810
  return 5;
9488
9811
  return hasComplexKeyword ? 3 : 1;
9489
9812
  }
9813
+ ensureSwarmModelReady(source) {
9814
+ if (this.session.model)
9815
+ return true;
9816
+ if (source === "singular") {
9817
+ this.showWarning("Cannot launch Swarm from /singular: no active model. Select one via /model and retry.");
9818
+ }
9819
+ else {
9820
+ this.showWarning("Cannot run /swarm: no active model selected. Configure /model first.");
9821
+ }
9822
+ return false;
9823
+ }
9824
+ resolveSwarmMaxParallel(input) {
9825
+ const requested = input.requested;
9826
+ if (typeof requested === "number" && Number.isFinite(requested)) {
9827
+ return Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, Math.floor(requested)));
9828
+ }
9829
+ const totalTasks = Math.max(1, input.plan.tasks.length);
9830
+ const initialFanout = Math.max(1, input.plan.tasks.filter((task) => task.depends_on.length === 0).length);
9831
+ const parallelizable = input.plan.tasks.filter((task) => task.concurrency_class === "implementation" || task.concurrency_class === "tests")
9832
+ .length;
9833
+ const sourceFloor = input.source === "singular" ? 4 : 3;
9834
+ const autoCap = Math.min(MAX_ORCHESTRATION_PARALLEL, 10);
9835
+ const heuristic = Math.max(sourceFloor, Math.ceil(totalTasks / 2), initialFanout, parallelizable >= 4 ? Math.ceil(parallelizable / 2) : 1);
9836
+ return Math.max(1, Math.min(autoCap, heuristic));
9837
+ }
9838
+ resolveSwarmDispatchTimeoutMs() {
9839
+ const dispatchTimeoutRaw = Number.parseInt(process.env.IOSM_SWARM_DISPATCH_TIMEOUT_MS ?? "", 10);
9840
+ if (Number.isInteger(dispatchTimeoutRaw) && dispatchTimeoutRaw > 0) {
9841
+ return Math.max(1_000, Math.min(1_800_000, dispatchTimeoutRaw));
9842
+ }
9843
+ return 180_000;
9844
+ }
9490
9845
  parseSwarmSpawnCandidates(output, parentTaskId) {
9491
9846
  const lines = output.split(/\r?\n/);
9492
9847
  const results = [];
@@ -9530,6 +9885,16 @@ export class InteractiveMode {
9530
9885
  }
9531
9886
  const profile = this.resolveSwarmTaskProfile(input.task);
9532
9887
  const delegateParallelHint = this.deriveSwarmTaskDelegateParallelHint(input.task);
9888
+ const requiresStrongDelegation = input.task.concurrency_class !== "analysis" &&
9889
+ input.task.concurrency_class !== "docs" &&
9890
+ (input.task.severity === "high" || delegateParallelHint >= 7);
9891
+ const minDelegatesRequired = !requiresStrongDelegation
9892
+ ? 0
9893
+ : delegateParallelHint >= 8
9894
+ ? 3
9895
+ : delegateParallelHint >= 5
9896
+ ? 2
9897
+ : 1;
9533
9898
  const safeDescription = input.task.brief.replace(/\s+/g, " ").trim().slice(0, 120).replace(/"/g, "'");
9534
9899
  const prompt = [
9535
9900
  `<swarm_task run_id="${input.meta.runId}" task_id="${input.task.id}" profile_hint="${profile}">`,
@@ -9542,10 +9907,15 @@ export class InteractiveMode {
9542
9907
  "",
9543
9908
  "Execution requirements:",
9544
9909
  `- Use the task tool exactly once with description="${safeDescription || input.task.id}", profile="${profile}", delegate_parallel_hint=${delegateParallelHint}, run_id="${input.meta.runId}", task_id="${input.task.id}".`,
9545
- `- Inside this single task call, prefer decomposition into <delegate_task> subtasks when delegate_parallel_hint >= 2 (target parallel fan-out up to ${delegateParallelHint}).`,
9546
- "- If decomposition is not beneficial, continue with single-agent execution (optional note: DELEGATION_IMPOSSIBLE: <reason>).",
9910
+ minDelegatesRequired > 0
9911
+ ? `- Inside this single task call, emit at least ${minDelegatesRequired} independent <delegate_task> subtasks (target parallel fan-out up to ${delegateParallelHint}).`
9912
+ : "- Delegation is optional for this task; keep execution focused.",
9913
+ minDelegatesRequired > 0
9914
+ ? '- If safe decomposition is impossible, output exactly one line: DELEGATION_IMPOSSIBLE: <reason>.'
9915
+ : "- If decomposition is not beneficial, continue with single-agent execution.",
9547
9916
  "- Keep edits inside declared scopes/touches. If scope expansion is required, explain and stop.",
9548
9917
  "- If blocked, respond with line: BLOCKED: <reason>",
9918
+ "- Return concise execution output; avoid long narrative if not needed.",
9549
9919
  "- Optional spawn candidates format: '- <description> | <path> | <change_type> | <low|medium|high>'",
9550
9920
  "</swarm_task>",
9551
9921
  ].join("\n");
@@ -9583,7 +9953,7 @@ export class InteractiveMode {
9583
9953
  resourceLoader,
9584
9954
  model,
9585
9955
  thinkingLevel: this.session.thinkingLevel,
9586
- profile: "full",
9956
+ profile: "meta",
9587
9957
  enableTaskTool: true,
9588
9958
  });
9589
9959
  swarmSession = created.session;
@@ -9600,6 +9970,14 @@ export class InteractiveMode {
9600
9970
  const taskErrors = [];
9601
9971
  let delegatedFailed = 0;
9602
9972
  let delegatedTasks = 0;
9973
+ let toolCallsStarted = 0;
9974
+ let toolCallsCompleted = 0;
9975
+ let assistantMessages = 0;
9976
+ let activeTool;
9977
+ const dispatchTimeoutMs = this.resolveSwarmDispatchTimeoutMs();
9978
+ let timedOut = false;
9979
+ let timeoutHandle;
9980
+ let detachStopListener;
9603
9981
  const delegatedFailureCauses = new Map();
9604
9982
  const accumulateFailureCauses = (raw) => {
9605
9983
  if (!raw || typeof raw !== "object")
@@ -9613,13 +9991,71 @@ export class InteractiveMode {
9613
9991
  delegatedFailureCauses.set(cause, (delegatedFailureCauses.get(cause) ?? 0) + numeric);
9614
9992
  }
9615
9993
  };
9994
+ const emitProgress = (progress) => {
9995
+ input.onProgress?.({
9996
+ activeTool,
9997
+ toolCallsStarted,
9998
+ toolCallsCompleted,
9999
+ assistantMessages,
10000
+ delegatedTasks,
10001
+ delegatedFailed,
10002
+ ...progress,
10003
+ });
10004
+ };
9616
10005
  const chunks = [];
9617
10006
  const unsubscribe = swarmSession.subscribe((event) => {
9618
- if (event.type === "tool_execution_start" && event.toolName === "task") {
9619
- taskToolCalls += 1;
10007
+ if (event.type === "tool_execution_start") {
10008
+ toolCallsStarted += 1;
10009
+ activeTool = event.toolName;
10010
+ if (event.toolName === "task") {
10011
+ taskToolCalls += 1;
10012
+ }
10013
+ emitProgress({
10014
+ phase: event.toolName ? `running ${event.toolName}` : "running",
10015
+ phaseState: "running",
10016
+ });
10017
+ return;
10018
+ }
10019
+ if (event.type === "tool_execution_update" && event.toolName === "task") {
10020
+ const partial = event.partialResult;
10021
+ const progressCandidate = partial?.details?.progress;
10022
+ if (progressCandidate && typeof progressCandidate === "object") {
10023
+ const progress = progressCandidate;
10024
+ const delegateItems = parseSubagentDelegateItems(progress.delegateItems);
10025
+ emitProgress({
10026
+ phase: typeof progress.message === "string" ? progress.message : undefined,
10027
+ phaseState: isSubagentPhaseState(progress.phase) ? progress.phase : undefined,
10028
+ cwd: typeof progress.cwd === "string" ? progress.cwd : undefined,
10029
+ activeTool: typeof progress.activeTool === "string" && progress.activeTool.trim().length > 0
10030
+ ? progress.activeTool.trim()
10031
+ : activeTool,
10032
+ toolCallsStarted: typeof progress.toolCallsStarted === "number" && Number.isFinite(progress.toolCallsStarted)
10033
+ ? progress.toolCallsStarted
10034
+ : undefined,
10035
+ toolCallsCompleted: typeof progress.toolCallsCompleted === "number" && Number.isFinite(progress.toolCallsCompleted)
10036
+ ? progress.toolCallsCompleted
10037
+ : undefined,
10038
+ assistantMessages: typeof progress.assistantMessages === "number" && Number.isFinite(progress.assistantMessages)
10039
+ ? progress.assistantMessages
10040
+ : undefined,
10041
+ delegateIndex: typeof progress.delegateIndex === "number" && Number.isFinite(progress.delegateIndex)
10042
+ ? progress.delegateIndex
10043
+ : undefined,
10044
+ delegateTotal: typeof progress.delegateTotal === "number" && Number.isFinite(progress.delegateTotal)
10045
+ ? progress.delegateTotal
10046
+ : undefined,
10047
+ delegateDescription: typeof progress.delegateDescription === "string" ? progress.delegateDescription : undefined,
10048
+ delegateProfile: typeof progress.delegateProfile === "string" ? progress.delegateProfile : undefined,
10049
+ delegateItems,
10050
+ });
10051
+ }
9620
10052
  return;
9621
10053
  }
9622
10054
  if (event.type === "tool_execution_end" && event.toolName === "task") {
10055
+ toolCallsCompleted += 1;
10056
+ if (activeTool === event.toolName) {
10057
+ activeTool = undefined;
10058
+ }
9623
10059
  const result = event.result;
9624
10060
  const details = result?.details;
9625
10061
  if (typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)) {
@@ -9629,38 +10065,127 @@ export class InteractiveMode {
9629
10065
  delegatedTasks += Math.max(0, details.delegatedTasks);
9630
10066
  }
9631
10067
  accumulateFailureCauses(details?.failureCauses);
10068
+ emitProgress({
10069
+ phase: event.isError ? "task tool failed" : "task tool completed",
10070
+ phaseState: "running",
10071
+ delegatedTasks: typeof details?.delegatedTasks === "number" && Number.isFinite(details.delegatedTasks)
10072
+ ? details.delegatedTasks
10073
+ : undefined,
10074
+ delegatedSucceeded: typeof details?.delegatedSucceeded === "number" && Number.isFinite(details.delegatedSucceeded)
10075
+ ? details.delegatedSucceeded
10076
+ : undefined,
10077
+ delegatedFailed: typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)
10078
+ ? details.delegatedFailed
10079
+ : undefined,
10080
+ });
9632
10081
  if (event.isError) {
9633
10082
  taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
9634
10083
  }
9635
10084
  return;
9636
10085
  }
10086
+ if (event.type === "tool_execution_end") {
10087
+ toolCallsCompleted += 1;
10088
+ if (activeTool === event.toolName) {
10089
+ activeTool = undefined;
10090
+ }
10091
+ emitProgress({
10092
+ phase: event.toolName ? `completed ${event.toolName}` : "running",
10093
+ phaseState: "running",
10094
+ });
10095
+ return;
10096
+ }
9637
10097
  if (event.type === "message_end" && event.message.role === "assistant") {
10098
+ assistantMessages += 1;
9638
10099
  for (const part of event.message.content) {
9639
10100
  if (part.type === "text" && part.text.trim()) {
9640
10101
  chunks.push(part.text.trim());
9641
10102
  }
9642
10103
  }
10104
+ emitProgress({
10105
+ phase: "drafting response",
10106
+ phaseState: "responding",
10107
+ });
9643
10108
  }
9644
10109
  });
9645
10110
  try {
9646
- await swarmSession.prompt(prompt, {
10111
+ emitProgress({
10112
+ phase: "booting subagent",
10113
+ phaseState: "starting",
10114
+ });
10115
+ if (input.stopSignal?.aborted) {
10116
+ return {
10117
+ taskId: input.task.id,
10118
+ status: "blocked",
10119
+ error: "Swarm run interrupted.",
10120
+ failureCause: "interrupted",
10121
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
10122
+ };
10123
+ }
10124
+ const promptPromise = swarmSession.prompt(prompt, {
9647
10125
  expandPromptTemplates: false,
9648
10126
  skipIosmAutopilot: true,
9649
10127
  skipOrchestrationDirective: true,
9650
10128
  source: "interactive",
9651
10129
  });
10130
+ // Guard against provider/model hangs in isolated swarm sessions.
10131
+ void promptPromise.catch(() => {
10132
+ // handled by race below
10133
+ });
10134
+ const timeoutPromise = new Promise((_, reject) => {
10135
+ timeoutHandle = setTimeout(() => {
10136
+ timedOut = true;
10137
+ void swarmSession.abort().catch(() => {
10138
+ // best effort
10139
+ });
10140
+ emitProgress({
10141
+ phase: "dispatch timeout",
10142
+ phaseState: "responding",
10143
+ });
10144
+ reject(new Error(`Swarm task dispatch timed out after ${dispatchTimeoutMs}ms.`));
10145
+ }, dispatchTimeoutMs);
10146
+ });
10147
+ const stopPromise = new Promise((_, reject) => {
10148
+ if (!input.stopSignal)
10149
+ return;
10150
+ const onAbort = () => {
10151
+ void swarmSession.abort().catch(() => {
10152
+ // best effort
10153
+ });
10154
+ emitProgress({
10155
+ phase: "dispatch interrupted",
10156
+ phaseState: "responding",
10157
+ });
10158
+ reject(new Error("Swarm task dispatch interrupted."));
10159
+ };
10160
+ if (input.stopSignal.aborted) {
10161
+ onAbort();
10162
+ return;
10163
+ }
10164
+ input.stopSignal.addEventListener("abort", onAbort, { once: true });
10165
+ detachStopListener = () => input.stopSignal?.removeEventListener("abort", onAbort);
10166
+ });
10167
+ await Promise.race([promptPromise, timeoutPromise, stopPromise]);
9652
10168
  }
9653
10169
  catch (error) {
9654
10170
  return {
9655
10171
  taskId: input.task.id,
9656
10172
  status: "error",
9657
10173
  error: error instanceof Error ? error.message : String(error),
10174
+ failureCause: error instanceof Error && /interrupted/i.test(error.message)
10175
+ ? "interrupted"
10176
+ : timedOut
10177
+ ? "timeout"
10178
+ : undefined,
9658
10179
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9659
10180
  };
9660
10181
  }
9661
10182
  finally {
10183
+ detachStopListener?.();
10184
+ if (timeoutHandle) {
10185
+ clearTimeout(timeoutHandle);
10186
+ }
9662
10187
  unsubscribe();
9663
- if (swarmSession.isStreaming) {
10188
+ if (timedOut || swarmSession.isStreaming) {
9664
10189
  await swarmSession.abort().catch(() => {
9665
10190
  // best effort
9666
10191
  });
@@ -9682,6 +10207,7 @@ export class InteractiveMode {
9682
10207
  taskId: input.task.id,
9683
10208
  status: "error",
9684
10209
  error: "No task tool call executed by assistant.",
10210
+ failureCause: "protocol_violation",
9685
10211
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9686
10212
  };
9687
10213
  }
@@ -9723,6 +10249,7 @@ export class InteractiveMode {
9723
10249
  if (!input.resumeState) {
9724
10250
  store.init(input.meta, input.plan, initialState);
9725
10251
  }
10252
+ const swarmTaskById = new Map(input.plan.tasks.map((task) => [task.id, task]));
9726
10253
  let rollingIndex = input.projectIndex;
9727
10254
  let localStopRequested = false;
9728
10255
  const refreshIncrementalIndex = () => {
@@ -9740,52 +10267,159 @@ export class InteractiveMode {
9740
10267
  rollingIndex = rebuilt;
9741
10268
  };
9742
10269
  this.swarmActiveRunId = input.runId;
10270
+ this.swarmStopRequested = false;
10271
+ this.swarmAbortController = new AbortController();
10272
+ this.footerDataProvider.setSwarmBusy(true);
10273
+ this.footer.invalidate();
9743
10274
  this.showStatus(`Swarm run started: ${input.runId}`);
9744
- const schedulerResult = await runSwarmScheduler({
9745
- runId: input.runId,
9746
- plan: input.plan,
9747
- contract: input.contract,
9748
- maxParallel: input.meta.maxParallel,
9749
- budgetUsd: input.budgetUsd,
9750
- existingState: initialState,
9751
- dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
9752
- meta: input.meta,
9753
- task,
9754
- runtime,
10275
+ let schedulerResult;
10276
+ try {
10277
+ schedulerResult = await runSwarmScheduler({
10278
+ runId: input.runId,
10279
+ plan: input.plan,
9755
10280
  contract: input.contract,
9756
- }),
9757
- confirmSpawn: async ({ candidate, parentTask }) => {
9758
- const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
9759
- if (!requiresConfirmation)
10281
+ maxParallel: input.meta.maxParallel,
10282
+ budgetUsd: input.budgetUsd,
10283
+ existingState: initialState,
10284
+ dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
10285
+ meta: input.meta,
10286
+ task,
10287
+ runtime,
10288
+ contract: input.contract,
10289
+ stopSignal: this.swarmAbortController?.signal,
10290
+ onProgress: (progress) => {
10291
+ this.updateSwarmSubagentProgress({
10292
+ runId: input.runId,
10293
+ taskId: task.id,
10294
+ task,
10295
+ profile: this.resolveSwarmTaskProfile(task),
10296
+ progress,
10297
+ });
10298
+ },
10299
+ }),
10300
+ confirmSpawn: async ({ candidate, parentTask }) => {
10301
+ const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
10302
+ if (!requiresConfirmation)
10303
+ return true;
10304
+ const choice = await this.showExtensionSelector([
10305
+ `/swarm spawn candidate requires confirmation`,
10306
+ `severity=${candidate.severity} task=${parentTask.id}`,
10307
+ `description=${candidate.description}`,
10308
+ `path=${candidate.path}`,
10309
+ ].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
10310
+ if (!choice || choice.startsWith("Reject")) {
10311
+ this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
10312
+ return false;
10313
+ }
10314
+ if (choice === "Abort run") {
10315
+ localStopRequested = true;
10316
+ this.showWarning("Swarm run marked to stop after current scheduling step.");
10317
+ return false;
10318
+ }
9760
10319
  return true;
9761
- const choice = await this.showExtensionSelector([
9762
- `/swarm spawn candidate requires confirmation`,
9763
- `severity=${candidate.severity} task=${parentTask.id}`,
9764
- `description=${candidate.description}`,
9765
- `path=${candidate.path}`,
9766
- ].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
9767
- if (!choice || choice.startsWith("Reject")) {
9768
- this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
9769
- return false;
9770
- }
9771
- if (choice === "Abort run") {
9772
- localStopRequested = true;
9773
- this.showWarning("Swarm run marked to stop after current scheduling step.");
9774
- return false;
9775
- }
9776
- return true;
9777
- },
9778
- onEvent: (event) => {
9779
- store.appendEvent(event);
9780
- },
9781
- onStateChanged: (state) => {
9782
- store.saveState(state);
9783
- store.saveCheckpoint(state);
9784
- refreshIncrementalIndex();
9785
- },
9786
- shouldStop: () => this.shutdownRequested || localStopRequested,
9787
- });
9788
- this.swarmActiveRunId = undefined;
10320
+ },
10321
+ dispatchTimeoutMs: Math.min(1_800_000, this.resolveSwarmDispatchTimeoutMs() + 5_000),
10322
+ onEvent: (event) => {
10323
+ store.appendEvent(event);
10324
+ if (event.type === "task_running" && event.taskId) {
10325
+ const task = swarmTaskById.get(event.taskId);
10326
+ if (task) {
10327
+ this.updateSwarmSubagentProgress({
10328
+ runId: input.runId,
10329
+ taskId: task.id,
10330
+ task,
10331
+ profile: this.resolveSwarmTaskProfile(task),
10332
+ progress: {
10333
+ phase: "starting subagent",
10334
+ phaseState: "starting",
10335
+ },
10336
+ });
10337
+ }
10338
+ this.showStatus(`Swarm ${event.taskId}: running`);
10339
+ }
10340
+ if (event.type === "task_done" && event.taskId) {
10341
+ this.finalizeSwarmSubagentDisplay({
10342
+ runId: input.runId,
10343
+ taskId: event.taskId,
10344
+ status: "done",
10345
+ });
10346
+ this.showStatus(`Swarm ${event.taskId}: done`);
10347
+ }
10348
+ if (event.type === "task_retry" && event.taskId) {
10349
+ const task = swarmTaskById.get(event.taskId);
10350
+ if (task) {
10351
+ this.updateSwarmSubagentProgress({
10352
+ runId: input.runId,
10353
+ taskId: event.taskId,
10354
+ task,
10355
+ profile: this.resolveSwarmTaskProfile(task),
10356
+ progress: {
10357
+ phase: event.message,
10358
+ phaseState: "running",
10359
+ },
10360
+ });
10361
+ }
10362
+ this.showWarning(`Swarm ${event.taskId}: ${event.message}`);
10363
+ }
10364
+ if (event.type === "task_error" && event.taskId) {
10365
+ this.finalizeSwarmSubagentDisplay({
10366
+ runId: input.runId,
10367
+ taskId: event.taskId,
10368
+ status: "error",
10369
+ errorMessage: event.message,
10370
+ });
10371
+ this.showWarning(`Swarm ${event.taskId} failed: ${event.message}`);
10372
+ }
10373
+ if (event.type === "task_blocked" && event.taskId) {
10374
+ this.finalizeSwarmSubagentDisplay({
10375
+ runId: input.runId,
10376
+ taskId: event.taskId,
10377
+ status: "error",
10378
+ errorMessage: event.message,
10379
+ });
10380
+ this.showWarning(`Swarm ${event.taskId} blocked: ${event.message}`);
10381
+ }
10382
+ if ((event.type === "run_blocked" || event.type === "run_failed" || event.type === "run_stopped") && event.message) {
10383
+ this.showWarning(`Swarm ${event.type.replace("run_", "")}: ${event.message}`);
10384
+ }
10385
+ },
10386
+ onStateChanged: (state) => {
10387
+ store.saveState(state);
10388
+ store.saveCheckpoint(state);
10389
+ refreshIncrementalIndex();
10390
+ },
10391
+ shouldStop: () => this.shutdownRequested || localStopRequested || this.swarmStopRequested,
10392
+ });
10393
+ }
10394
+ catch (error) {
10395
+ const message = error instanceof Error ? error.message : String(error);
10396
+ const failedState = store.loadState() ?? initialState;
10397
+ failedState.status = "failed";
10398
+ failedState.lastError = message;
10399
+ failedState.updatedAt = new Date().toISOString();
10400
+ store.appendEvent({
10401
+ type: "run_failed",
10402
+ timestamp: new Date().toISOString(),
10403
+ runId: input.runId,
10404
+ tick: failedState.tick,
10405
+ message,
10406
+ });
10407
+ store.saveState(failedState);
10408
+ store.saveCheckpoint(failedState);
10409
+ this.finalizeSwarmRunSubagentDisplays(input.runId, message);
10410
+ this.showWarning(`Swarm run failed unexpectedly: ${message}`);
10411
+ return;
10412
+ }
10413
+ finally {
10414
+ this.swarmActiveRunId = undefined;
10415
+ this.swarmStopRequested = false;
10416
+ this.swarmAbortController = undefined;
10417
+ this.footerDataProvider.setSwarmBusy(false);
10418
+ this.footer.invalidate();
10419
+ }
10420
+ if (schedulerResult.state.status !== "completed") {
10421
+ this.finalizeSwarmRunSubagentDisplays(input.runId, schedulerResult.state.lastError ?? `Swarm run ${schedulerResult.state.status}`);
10422
+ }
9789
10423
  const taskStates = Object.values(schedulerResult.state.tasks);
9790
10424
  const doneCount = taskStates.filter((task) => task.status === "done").length;
9791
10425
  const errorCount = taskStates.filter((task) => task.status === "error").length;
@@ -9841,6 +10475,8 @@ export class InteractiveMode {
9841
10475
  ].join("\n"));
9842
10476
  }
9843
10477
  async runSwarmFromTask(task, options) {
10478
+ if (!this.ensureSwarmModelReady("plain"))
10479
+ return;
9844
10480
  const contract = await this.ensureSwarmEffectiveContract(task);
9845
10481
  if (!contract)
9846
10482
  return;
@@ -9855,7 +10491,11 @@ export class InteractiveMode {
9855
10491
  index: indexInfo.index,
9856
10492
  });
9857
10493
  const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9858
- const maxParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, options.maxParallel ?? 3));
10494
+ const maxParallel = this.resolveSwarmMaxParallel({
10495
+ requested: options.maxParallel,
10496
+ plan,
10497
+ source: "plain",
10498
+ });
9859
10499
  const meta = this.buildSwarmRunMeta({
9860
10500
  runId,
9861
10501
  source: "plain",
@@ -9877,6 +10517,8 @@ export class InteractiveMode {
9877
10517
  });
9878
10518
  }
9879
10519
  async runSwarmFromSingular(input) {
10520
+ if (!this.ensureSwarmModelReady("singular"))
10521
+ return;
9880
10522
  const analysis = this.loadSingularAnalysisByRunId(input.runId);
9881
10523
  if (!analysis) {
9882
10524
  this.showWarning(`Singular run not found: ${input.runId}`);
@@ -9899,7 +10541,11 @@ export class InteractiveMode {
9899
10541
  index: indexInfo.index,
9900
10542
  });
9901
10543
  const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9902
- const maxParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, input.maxParallel ?? 3));
10544
+ const maxParallel = this.resolveSwarmMaxParallel({
10545
+ requested: input.maxParallel,
10546
+ plan,
10547
+ source: "singular",
10548
+ });
9903
10549
  const meta = this.buildSwarmRunMeta({
9904
10550
  runId,
9905
10551
  source: "singular",
@@ -10087,6 +10733,10 @@ export class InteractiveMode {
10087
10733
  });
10088
10734
  }
10089
10735
  async handleSwarmCommand(text) {
10736
+ if (this.swarmActiveRunId) {
10737
+ this.showWarning(`Swarm run already in progress: ${this.swarmActiveRunId}. Use /swarm watch.`);
10738
+ return;
10739
+ }
10090
10740
  if (this.session.isStreaming) {
10091
10741
  this.showWarning("Cannot run /swarm while the agent is processing another request.");
10092
10742
  return;
@@ -10300,6 +10950,9 @@ export class InteractiveMode {
10300
10950
  this.showWarning("--max-parallel is only valid with --parallel mode.");
10301
10951
  return undefined;
10302
10952
  }
10953
+ if (mode === "parallel" && maxParallel === undefined) {
10954
+ maxParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, agents));
10955
+ }
10303
10956
  if (maxParallel !== undefined && maxParallel > agents) {
10304
10957
  maxParallel = agents;
10305
10958
  }
@@ -10366,11 +11019,24 @@ export class InteractiveMode {
10366
11019
  const currentCwd = this.sessionManager.getCwd();
10367
11020
  const assignments = [];
10368
11021
  const assignmentRecords = [];
11022
+ const dependencyEdges = parsed.dependencies?.reduce((sum, entry) => sum + entry.dependsOn.length, 0) ?? 0;
11023
+ const defaultAssignmentProfile = this.resolveOrchestrateDefaultAssignmentProfile(parsed);
11024
+ const delegateParallelHints = [];
10369
11025
  for (let index = 0; index < parsed.agents; index++) {
10370
- const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? (this.activeProfileName || "full");
11026
+ const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? defaultAssignmentProfile;
10371
11027
  const assignmentCwd = parsed.cwds?.[index] ?? ".";
10372
11028
  const assignmentLock = parsed.locks?.[index];
10373
11029
  const dependsOn = parsed.dependencies?.find((entry) => entry.agent === index + 1)?.dependsOn ?? [];
11030
+ const delegateParallelHint = this.deriveOrchestrateDelegateParallelHint({
11031
+ task: parsed.task,
11032
+ mode: parsed.mode,
11033
+ agents: parsed.agents,
11034
+ maxParallel: parsed.maxParallel,
11035
+ dependencyEdges,
11036
+ hasLock: !!assignmentLock,
11037
+ hasDependencies: dependsOn.length > 0,
11038
+ });
11039
+ delegateParallelHints.push(delegateParallelHint);
10374
11040
  const resolvedCwd = path.resolve(currentCwd, assignmentCwd);
10375
11041
  assignmentRecords.push({
10376
11042
  profile: assignmentProfile,
@@ -10378,7 +11044,8 @@ export class InteractiveMode {
10378
11044
  lockKey: assignmentLock,
10379
11045
  dependsOn,
10380
11046
  });
10381
- assignments.push(`- agent ${index + 1}: profile=${assignmentProfile} cwd=${resolvedCwd}${assignmentLock ? ` lock_key=${assignmentLock}` : ""}${parsed.isolation === "worktree" ? " isolation=worktree" : ""}${dependsOn.length > 0 ? ` depends_on=${dependsOn.join("|")}` : ""}`);
11047
+ assignments.push(`- agent ${index + 1}: profile=${assignmentProfile} cwd=${resolvedCwd}${assignmentLock ? ` lock_key=${assignmentLock}` : ""}${parsed.isolation === "worktree" ? " isolation=worktree" : ""}${dependsOn.length > 0 ? ` depends_on=${dependsOn.join("|")}` : ""} delegate_parallel_hint=${delegateParallelHint}
11048
+ }`);
10382
11049
  }
10383
11050
  const teamRun = createTeamRun({
10384
11051
  cwd: currentCwd,
@@ -10391,8 +11058,16 @@ export class InteractiveMode {
10391
11058
  const runAssignments = teamRun.tasks.map((task, index) => `${assignments[index]} run_id=${teamRun.runId} task_id=${task.id}`);
10392
11059
  const taskCallHints = teamRun.tasks.map((task, index) => {
10393
11060
  const assignment = assignmentRecords[index];
10394
- return `- task_call_${index + 1}: description="agent ${index + 1} execution" profile="${assignment.profile}" cwd="${assignment.cwd}" run_id="${teamRun.runId}" task_id="${task.id}"${assignment.lockKey ? ` lock_key="${assignment.lockKey}"` : ""}${parsed.isolation === "worktree" ? ' isolation="worktree"' : ""}`;
11061
+ const hint = delegateParallelHints[index] ?? 1;
11062
+ return `- task_call_${index + 1}: description="agent ${index + 1} execution" profile="${assignment.profile}" cwd="${assignment.cwd}" run_id="${teamRun.runId}" task_id="${task.id}"${assignment.lockKey ? ` lock_key="${assignment.lockKey}"` : ""}${parsed.isolation === "worktree" ? ' isolation="worktree"' : ""} delegate_parallel_hint=${hint}`;
10395
11063
  });
11064
+ if (parsed.mode === "parallel" &&
11065
+ !parsed.profile &&
11066
+ !(parsed.profiles && parsed.profiles.length > 0) &&
11067
+ defaultAssignmentProfile === "meta" &&
11068
+ this.activeProfileName !== "meta") {
11069
+ this.showStatus("Orchestrate auto-profile: using `meta` workers for stronger fan-out and nested delegation.");
11070
+ }
10396
11071
  this.chatContainer.addChild(new Spacer(1));
10397
11072
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "◆ ")) + theme.bold("/orchestrate") + theme.fg("muted", ` ${currentCwd}`), 1, 0));
10398
11073
  this.ui.requestRender();
@@ -10407,6 +11082,9 @@ export class InteractiveMode {
10407
11082
  "- for parallel mode, emit all independent task calls in one assistant response",
10408
11083
  "- in parallel mode, use parallel tool-call style (<use_parallel_tool_calls>)",
10409
11084
  "- when assignment lines include depends_on, still emit one task call per assignment; runtime enforces dependency gating",
11085
+ "- include delegate_parallel_hint from each assignment/task_call hint in every corresponding task tool call",
11086
+ "- for delegate_parallel_hint >= 2, split child work into nested <delegate_task> streams unless impossible",
11087
+ "- if nested split is impossible for a non-trivial stream, emit one line: DELEGATION_IMPOSSIBLE: <reason>",
10410
11088
  "- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
10411
11089
  "- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
10412
11090
  "- include run_id and task_id from each assignment in the task tool arguments",
@@ -12394,6 +13072,10 @@ The agent will automatically receive IOSM context on every turn.`;
12394
13072
  }
12395
13073
  }
12396
13074
  async handleBashCommand(command, excludeFromContext = false) {
13075
+ if (isReadOnlyProfileName(this.activeProfileName)) {
13076
+ this.showWarning(`Bash is disabled in ${this.activeProfileName} profile. Switch to full/meta/iosm (Shift+Tab).`);
13077
+ return;
13078
+ }
12397
13079
  const extensionRunner = this.session.extensionRunner;
12398
13080
  // Emit user_bash event to let extensions intercept
12399
13081
  const eventResult = extensionRunner