iosm-cli 0.2.3 → 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 (93) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +43 -10
  3. package/dist/cli/args.d.ts +1 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +1 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-profiles.d.ts +2 -1
  8. package/dist/core/agent-profiles.d.ts.map +1 -1
  9. package/dist/core/agent-profiles.js +18 -5
  10. package/dist/core/agent-profiles.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +7 -0
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +64 -15
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/agent-teams.d.ts.map +1 -1
  16. package/dist/core/agent-teams.js +90 -19
  17. package/dist/core/agent-teams.js.map +1 -1
  18. package/dist/core/extensions/types.d.ts +1 -1
  19. package/dist/core/extensions/types.d.ts.map +1 -1
  20. package/dist/core/extensions/types.js.map +1 -1
  21. package/dist/core/footer-data-provider.d.ts +6 -1
  22. package/dist/core/footer-data-provider.d.ts.map +1 -1
  23. package/dist/core/footer-data-provider.js +9 -0
  24. package/dist/core/footer-data-provider.js.map +1 -1
  25. package/dist/core/orchestration-limits.d.ts +6 -0
  26. package/dist/core/orchestration-limits.d.ts.map +1 -0
  27. package/dist/core/orchestration-limits.js +6 -0
  28. package/dist/core/orchestration-limits.js.map +1 -0
  29. package/dist/core/parallel-task-agent.d.ts +23 -1
  30. package/dist/core/parallel-task-agent.d.ts.map +1 -1
  31. package/dist/core/parallel-task-agent.js +110 -20
  32. package/dist/core/parallel-task-agent.js.map +1 -1
  33. package/dist/core/sdk.d.ts +6 -1
  34. package/dist/core/sdk.d.ts.map +1 -1
  35. package/dist/core/sdk.js +34 -6
  36. package/dist/core/sdk.js.map +1 -1
  37. package/dist/core/shared-memory.d.ts +2 -2
  38. package/dist/core/shared-memory.d.ts.map +1 -1
  39. package/dist/core/shared-memory.js +220 -91
  40. package/dist/core/shared-memory.js.map +1 -1
  41. package/dist/core/singular.d.ts.map +1 -1
  42. package/dist/core/singular.js +3 -1
  43. package/dist/core/singular.js.map +1 -1
  44. package/dist/core/subagents.d.ts +1 -1
  45. package/dist/core/subagents.d.ts.map +1 -1
  46. package/dist/core/subagents.js +32 -10
  47. package/dist/core/subagents.js.map +1 -1
  48. package/dist/core/swarm/planner.d.ts.map +1 -1
  49. package/dist/core/swarm/planner.js +200 -12
  50. package/dist/core/swarm/planner.js.map +1 -1
  51. package/dist/core/swarm/scheduler.d.ts +2 -0
  52. package/dist/core/swarm/scheduler.d.ts.map +1 -1
  53. package/dist/core/swarm/scheduler.js +87 -6
  54. package/dist/core/swarm/scheduler.js.map +1 -1
  55. package/dist/core/system-prompt.d.ts.map +1 -1
  56. package/dist/core/system-prompt.js +1 -0
  57. package/dist/core/system-prompt.js.map +1 -1
  58. package/dist/core/tools/ast-grep.d.ts.map +1 -1
  59. package/dist/core/tools/ast-grep.js +2 -0
  60. package/dist/core/tools/ast-grep.js.map +1 -1
  61. package/dist/core/tools/edit.d.ts +8 -4
  62. package/dist/core/tools/edit.d.ts.map +1 -1
  63. package/dist/core/tools/edit.js +18 -3
  64. package/dist/core/tools/edit.js.map +1 -1
  65. package/dist/core/tools/index.d.ts +9 -7
  66. package/dist/core/tools/index.d.ts.map +1 -1
  67. package/dist/core/tools/shared-memory.d.ts.map +1 -1
  68. package/dist/core/tools/shared-memory.js +34 -6
  69. package/dist/core/tools/shared-memory.js.map +1 -1
  70. package/dist/core/tools/task.d.ts +10 -3
  71. package/dist/core/tools/task.d.ts.map +1 -1
  72. package/dist/core/tools/task.js +830 -184
  73. package/dist/core/tools/task.js.map +1 -1
  74. package/dist/core/tools/todo.d.ts +10 -10
  75. package/dist/core/tools/todo.d.ts.map +1 -1
  76. package/dist/core/tools/todo.js +135 -17
  77. package/dist/core/tools/todo.js.map +1 -1
  78. package/dist/core/tools/yq.d.ts.map +1 -1
  79. package/dist/core/tools/yq.js +2 -0
  80. package/dist/core/tools/yq.js.map +1 -1
  81. package/dist/modes/interactive/components/footer.d.ts +1 -1
  82. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/footer.js +10 -9
  84. package/dist/modes/interactive/components/footer.js.map +1 -1
  85. package/dist/modes/interactive/interactive-mode.d.ts +22 -0
  86. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  87. package/dist/modes/interactive/interactive-mode.js +957 -75
  88. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  89. package/docs/cli-reference.md +8 -0
  90. package/docs/configuration.md +5 -1
  91. package/docs/interactive-mode.md +7 -1
  92. package/docs/orchestration-and-subagents.md +5 -0
  93. package/package.json +1 -1
@@ -10,11 +10,12 @@ 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";
17
17
  import { createCompactionSummaryMessage, INTERNAL_UI_META_CUSTOM_TYPE, isInternalUiMetaDetails, } from "../../core/messages.js";
18
+ import { MAX_ORCHESTRATION_AGENTS, MAX_ORCHESTRATION_PARALLEL, MAX_SUBAGENT_DELEGATE_PARALLEL, } from "../../core/orchestration-limits.js";
18
19
  import { loadModelsDevProviderCatalog, } from "../../core/models-dev-provider-catalog.js";
19
20
  import { ModelRegistry } from "../../core/model-registry.js";
20
21
  import { MODELS_DEV_PROVIDERS } from "../../core/models-dev-providers.js";
@@ -75,6 +76,212 @@ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMar
75
76
  function isExpandable(obj) {
76
77
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
77
78
  }
79
+ export function resolveStreamingSubmissionMode(input) {
80
+ if (input.configuredMode === "meta" &&
81
+ input.activeProfileName === "meta" &&
82
+ (input.activeSubagentCount > 0 || input.activeAssistantOrchestrationContext)) {
83
+ return "followUp";
84
+ }
85
+ return input.configuredMode;
86
+ }
87
+ function parseRequestedParallelAgentCount(text) {
88
+ const patterns = [
89
+ /(\d+)\s+(?:parallel|concurrent)\s+agents?/i,
90
+ /(\d+)\s+agents?/i,
91
+ /(\d+)\s+паралл[\p{L}\p{N}_-]*\s+агент[\p{L}\p{N}_-]*/iu,
92
+ /(\d+)\s+агент[\p{L}\p{N}_-]*/iu,
93
+ ];
94
+ for (const pattern of patterns) {
95
+ const match = text.match(pattern);
96
+ const parsed = match?.[1] ? Number.parseInt(match[1], 10) : Number.NaN;
97
+ if (Number.isInteger(parsed) && parsed >= 1) {
98
+ return Math.max(1, Math.min(MAX_ORCHESTRATION_AGENTS, parsed));
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ function deriveMetaRequiredTopLevelTaskCalls(userInput, taskPlanSnapshot) {
104
+ const requested = parseRequestedParallelAgentCount(userInput);
105
+ if (requested)
106
+ return requested;
107
+ if (!taskPlanSnapshot)
108
+ return undefined;
109
+ return Math.max(2, Math.min(3, taskPlanSnapshot.totalSteps));
110
+ }
111
+ function extractTaskToolErrorText(result) {
112
+ if (!result || typeof result !== "object")
113
+ return undefined;
114
+ const candidate = result;
115
+ if (typeof candidate.error === "string" && candidate.error.trim())
116
+ return candidate.error.trim();
117
+ if (typeof candidate.output === "string" && candidate.output.trim())
118
+ return candidate.output.trim();
119
+ return undefined;
120
+ }
121
+ function buildMetaParallelismCorrection(input) {
122
+ const validationFailure = input.taskToolError && /Validation failed for tool "task"/i.test(input.taskToolError)
123
+ ? input.taskToolError.split("\n").slice(0, 3).join("\n")
124
+ : undefined;
125
+ return [
126
+ "[META_PARALLELISM_CORRECTION]",
127
+ `Meta runtime correction: this run currently has ${input.launchedTopLevelTasks} top-level task call(s), but it should have at least ${input.requiredTopLevelTasks}.`,
128
+ "Stop manual sequential execution in the main agent and convert the execution graph into parallel task calls now.",
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,
133
+ "Each task tool call MUST include description and prompt.",
134
+ 'If you want a custom subagent, pass it via agent="name"; keep profile set to the capability profile, not the custom subagent name.',
135
+ input.rawRootDelegateBlocks && input.rawRootDelegateBlocks > 0
136
+ ? `You emitted ${input.rawRootDelegateBlocks} raw root-level <delegate_task> block(s). Those are not executed automatically in the parent session. Convert each one into an actual top-level task tool call now.`
137
+ : undefined,
138
+ "If a child workstream is still broad, require nested <delegate_task> fan-out instead of letting one child do everything alone.",
139
+ input.taskPlanSnapshot
140
+ ? `You already produced a complex plan with ${input.taskPlanSnapshot.totalSteps} steps. Turn that plan into parallel execution now.`
141
+ : undefined,
142
+ validationFailure ? `Previous task validation failure:\n${validationFailure}` : undefined,
143
+ "If you truly cannot split safely, output exactly one line: DELEGATION_IMPOSSIBLE: <precise reason>.",
144
+ "[/META_PARALLELISM_CORRECTION]",
145
+ ]
146
+ .filter((line) => typeof line === "string" && line.trim().length > 0)
147
+ .join("\n");
148
+ }
149
+ async function promptMetaWithParallelismGuard(input) {
150
+ let taskToolCalls = 0;
151
+ let nonTaskToolCalls = 0;
152
+ let taskPlanSnapshot;
153
+ let taskToolError;
154
+ let rawRootDelegateBlocks = 0;
155
+ let delegationImpossibleDeclared = false;
156
+ let correctionText;
157
+ let distinctTaskWorkers = new Set();
158
+ let delegatedChildTasksSeen = 0;
159
+ const maybeScheduleCorrection = (options) => {
160
+ const requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
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) {
170
+ return;
171
+ }
172
+ const hasComplexPlan = !!taskPlanSnapshot;
173
+ const hasTaskFailure = !!taskToolError;
174
+ const hasRawRootDelegates = rawRootDelegateBlocks > 0;
175
+ if (needsMoreTopLevelTasks && !hasTaskFailure && !hasRawRootDelegates) {
176
+ const minimumNonTaskCalls = hasComplexPlan ? 0 : 2;
177
+ if (!options?.finalize && nonTaskToolCalls < minimumNonTaskCalls) {
178
+ return;
179
+ }
180
+ }
181
+ correctionText = buildMetaParallelismCorrection({
182
+ requiredTopLevelTasks,
183
+ launchedTopLevelTasks: taskToolCalls,
184
+ taskPlanSnapshot,
185
+ taskToolError,
186
+ rawRootDelegateBlocks,
187
+ workerDiversityMissing,
188
+ distinctWorkers: distinctTaskWorkers.size,
189
+ });
190
+ };
191
+ const unsubscribe = input.session.subscribe((event) => {
192
+ if (event.type === "message_end" && event.message.role === "custom") {
193
+ if (event.message.customType === TASK_PLAN_CUSTOM_TYPE && isTaskPlanSnapshot(event.message.details)) {
194
+ taskPlanSnapshot = event.message.details;
195
+ maybeScheduleCorrection();
196
+ }
197
+ return;
198
+ }
199
+ if (event.type === "message_end" && event.message.role === "assistant") {
200
+ const rawText = extractAssistantText(event.message);
201
+ rawRootDelegateBlocks += (rawText.match(/<delegate_task\b/gi) ?? []).length;
202
+ delegationImpossibleDeclared =
203
+ delegationImpossibleDeclared || /^\s*DELEGATION_IMPOSSIBLE\s*:/im.test(rawText);
204
+ maybeScheduleCorrection();
205
+ return;
206
+ }
207
+ if (event.type === "tool_execution_start") {
208
+ if (event.toolName === "task") {
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
+ }
221
+ }
222
+ else {
223
+ nonTaskToolCalls += 1;
224
+ }
225
+ maybeScheduleCorrection();
226
+ return;
227
+ }
228
+ if (event.type === "tool_execution_end" && event.toolName === "task" && event.isError) {
229
+ taskToolError = extractTaskToolErrorText(event.result) ?? taskToolError;
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();
240
+ }
241
+ });
242
+ try {
243
+ await input.session.prompt(input.userInput);
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;
250
+ if (correctionText &&
251
+ requiredTopLevelTasks &&
252
+ (taskToolCalls < requiredTopLevelTasks || workerDiversityMissingAfterRun) &&
253
+ !delegationImpossibleDeclared) {
254
+ await input.session.prompt(correctionText, {
255
+ expandPromptTemplates: false,
256
+ skipOrchestrationDirective: true,
257
+ source: "interactive",
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
+ });
279
+ }
280
+ }
281
+ finally {
282
+ unsubscribe();
283
+ }
284
+ }
78
285
  const IOSM_PROFILE_ONLY_COMMANDS = new Set(["iosm", "cycle-list", "cycle-plan", "cycle-status", "cycle-report"]);
79
286
  const CHECKPOINT_LABEL_PREFIX = "checkpoint:";
80
287
  const INTERRUPT_ABORT_TIMEOUT_MS = 8_000;
@@ -402,6 +609,8 @@ export class InteractiveMode {
402
609
  this.sessionAllowedToolSignatures = new Set();
403
610
  this.singularLastEffectiveContract = {};
404
611
  this.swarmActiveRunId = undefined;
612
+ this.swarmStopRequested = false;
613
+ this.swarmAbortController = undefined;
405
614
  this.lastSigintTime = 0;
406
615
  this.lastEscapeTime = 0;
407
616
  this.changelogMarkdown = undefined;
@@ -411,6 +620,8 @@ export class InteractiveMode {
411
620
  // Streaming message tracking
412
621
  this.streamingComponent = undefined;
413
622
  this.streamingMessage = undefined;
623
+ this.currentTurnSawAssistantMessage = false;
624
+ this.currentTurnSawTaskToolCall = false;
414
625
  // Tool execution tracking: toolCallId -> component
415
626
  this.pendingTools = new Map();
416
627
  // Subagent execution tracking with live progress/metadata for task tool calls.
@@ -589,6 +800,17 @@ export class InteractiveMode {
589
800
  }
590
801
  }
591
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
+ }
592
814
  for (const rule of this.permissionDenyRules) {
593
815
  if (this.matchesPermissionRule(rule, request)) {
594
816
  this.showWarning(`Denied by rule: ${rule}`);
@@ -1990,7 +2212,7 @@ export class InteractiveMode {
1990
2212
  sessionManager: this.sessionManager,
1991
2213
  modelRegistry: this.session.modelRegistry,
1992
2214
  model: this.session.model,
1993
- isIdle: () => !this.session.isStreaming,
2215
+ isIdle: () => !this.session.isStreaming && this.swarmActiveRunId === undefined,
1994
2216
  abort: () => this.session.abort(),
1995
2217
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
1996
2218
  shutdown: () => {
@@ -2562,6 +2784,7 @@ export class InteractiveMode {
2562
2784
  this.session.isRetrying ||
2563
2785
  this.iosmAutomationRun !== undefined ||
2564
2786
  this.iosmVerificationSession !== undefined ||
2787
+ this.swarmActiveRunId !== undefined ||
2565
2788
  this.singularAnalysisSession !== undefined ||
2566
2789
  queuedMessages.steering.length > 0 ||
2567
2790
  queuedMessages.followUp.length > 0 ||
@@ -2917,9 +3140,18 @@ export class InteractiveMode {
2917
3140
  // If streaming, use configured stream input mode (meta/followUp/steer)
2918
3141
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
2919
3142
  if (this.session.isStreaming) {
3143
+ const streamingBehavior = resolveStreamingSubmissionMode({
3144
+ configuredMode: this.session.streamInputMode,
3145
+ activeProfileName: this.activeProfileName,
3146
+ activeSubagentCount: this.subagentComponents.size,
3147
+ activeAssistantOrchestrationContext: this.activeAssistantOrchestrationContext,
3148
+ });
2920
3149
  this.editor.addToHistory?.(text);
2921
3150
  this.editor.setText("");
2922
- await this.session.prompt(text, { streamingBehavior: this.session.streamInputMode });
3151
+ await this.session.prompt(text, { streamingBehavior });
3152
+ if (streamingBehavior !== this.session.streamInputMode) {
3153
+ this.showStatus("Queued follow-up until meta orchestration completes");
3154
+ }
2923
3155
  this.updatePendingMessagesDisplay();
2924
3156
  this.ui.requestRender();
2925
3157
  return;
@@ -2959,6 +3191,173 @@ export class InteractiveMode {
2959
3191
  durationMs: Date.now() - subagent.startTime,
2960
3192
  });
2961
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
+ }
2962
3361
  ensureSubagentElapsedTimer() {
2963
3362
  if (this.subagentElapsedTimer || this.subagentComponents.size === 0) {
2964
3363
  return;
@@ -2998,6 +3397,8 @@ export class InteractiveMode {
2998
3397
  this.footer.invalidate();
2999
3398
  switch (event.type) {
3000
3399
  case "agent_start":
3400
+ this.currentTurnSawAssistantMessage = false;
3401
+ this.currentTurnSawTaskToolCall = false;
3001
3402
  // Restore main escape handler if retry handler is still active
3002
3403
  // (retry success event fires later, but we need main handler now)
3003
3404
  if (this.retryEscapeHandler) {
@@ -3034,6 +3435,7 @@ export class InteractiveMode {
3034
3435
  this.ui.requestRender();
3035
3436
  }
3036
3437
  else if (event.message.role === "assistant") {
3438
+ this.currentTurnSawAssistantMessage = true;
3037
3439
  this.activeAssistantOrchestrationContext = this.pendingAssistantOrchestrationContexts > 0;
3038
3440
  if (this.activeAssistantOrchestrationContext) {
3039
3441
  this.pendingAssistantOrchestrationContexts -= 1;
@@ -3084,6 +3486,7 @@ export class InteractiveMode {
3084
3486
  if (this.streamingComponent && event.message.role === "assistant") {
3085
3487
  this.streamingMessage = event.message;
3086
3488
  let errorMessage;
3489
+ let interruptedStopReason;
3087
3490
  if (this.streamingMessage.stopReason === "aborted") {
3088
3491
  const retryAttempt = this.session.retryAttempt;
3089
3492
  errorMessage =
@@ -3091,9 +3494,13 @@ export class InteractiveMode {
3091
3494
  ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
3092
3495
  : "Operation aborted";
3093
3496
  this.streamingMessage.errorMessage = errorMessage;
3497
+ interruptedStopReason = "aborted";
3094
3498
  }
3095
3499
  this.streamingComponent.updateContent(this.sanitizeAssistantDisplayMessage(this.streamingMessage));
3096
3500
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
3501
+ if (this.streamingMessage.stopReason === "error") {
3502
+ interruptedStopReason = "error";
3503
+ }
3097
3504
  if (!errorMessage) {
3098
3505
  errorMessage = this.streamingMessage.errorMessage || "Error";
3099
3506
  }
@@ -3111,6 +3518,9 @@ export class InteractiveMode {
3111
3518
  component.setArgsComplete();
3112
3519
  }
3113
3520
  }
3521
+ if (interruptedStopReason) {
3522
+ this.showMetaModeInterruptionHint(interruptedStopReason);
3523
+ }
3114
3524
  this.streamingComponent = undefined;
3115
3525
  this.streamingMessage = undefined;
3116
3526
  this.activeAssistantOrchestrationContext = false;
@@ -3119,6 +3529,9 @@ export class InteractiveMode {
3119
3529
  this.ui.requestRender();
3120
3530
  break;
3121
3531
  case "tool_execution_start": {
3532
+ if (event.toolName === "task") {
3533
+ this.currentTurnSawTaskToolCall = true;
3534
+ }
3122
3535
  if (event.toolName === "task" && !this.subagentComponents.has(event.toolCallId)) {
3123
3536
  const staleTaskComponent = this.pendingTools.get(event.toolCallId);
3124
3537
  if (staleTaskComponent) {
@@ -3330,6 +3743,9 @@ export class InteractiveMode {
3330
3743
  break;
3331
3744
  }
3332
3745
  case "agent_end":
3746
+ if (this.activeProfileName === "meta" && !this.currentTurnSawAssistantMessage) {
3747
+ this.showMetaModeInterruptionHint("error");
3748
+ }
3333
3749
  if (this.loadingAnimation) {
3334
3750
  this.loadingAnimation.stop();
3335
3751
  this.loadingAnimation = undefined;
@@ -3343,6 +3759,7 @@ export class InteractiveMode {
3343
3759
  this.pendingTools.clear();
3344
3760
  this.subagentComponents.clear();
3345
3761
  this.clearSubagentElapsedTimer();
3762
+ this.currentTurnSawAssistantMessage = false;
3346
3763
  await this.checkShutdownRequested();
3347
3764
  this.ui.requestRender();
3348
3765
  break;
@@ -3459,7 +3876,12 @@ export class InteractiveMode {
3459
3876
  return;
3460
3877
  if (message.details.kind !== "orchestration_context")
3461
3878
  return;
3462
- this.pendingAssistantOrchestrationContexts += 1;
3879
+ // Hide assistant prose only for explicit legacy orchestration contracts.
3880
+ // META profile guidance should not suppress normal chat/task responses.
3881
+ const rawPrompt = message.details.rawPrompt ?? "";
3882
+ if (rawPrompt.includes("[ORCHESTRATION_DIRECTIVE]")) {
3883
+ this.pendingAssistantOrchestrationContexts += 1;
3884
+ }
3463
3885
  if (message.details.rawPrompt && message.details.displayText) {
3464
3886
  this.pendingInternalUserDisplayAliases.push({
3465
3887
  rawPrompt: message.details.rawPrompt,
@@ -3490,6 +3912,21 @@ export class InteractiveMode {
3490
3912
  this.lastStatusText = text;
3491
3913
  this.ui.requestRender();
3492
3914
  }
3915
+ showMetaModeInterruptionHint(reason) {
3916
+ if (this.activeProfileName !== "meta")
3917
+ return;
3918
+ const reasonText = reason === "error" ? "response failed unexpectedly" : "response was interrupted";
3919
+ this.showWarning(`META mode ${reasonText}. ` +
3920
+ "Please repeat your request following META profile rules: concrete repository task (goal + scope + constraints + expected output). " +
3921
+ "For conversational chat, switch profile to `full` (Shift+Tab).");
3922
+ }
3923
+ showMetaModeProfileHint() {
3924
+ if (this.activeProfileName !== "meta")
3925
+ return;
3926
+ this.showWarning("META mode is orchestration-first. " +
3927
+ "Send concrete repository tasks (goal + scope + constraints + expected output). " +
3928
+ "For conversational chat, switch profile to `full` (Shift+Tab).");
3929
+ }
3493
3930
  showProgressLine(message) {
3494
3931
  this.chatContainer.addChild(new Spacer(1));
3495
3932
  this.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
@@ -3731,6 +4168,15 @@ export class InteractiveMode {
3731
4168
  // =========================================================================
3732
4169
  handleCtrlC() {
3733
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
+ }
3734
4180
  if (this.iosmAutomationRun) {
3735
4181
  if (this.iosmAutomationRun.cancelRequested && now - this.lastSigintTime < 500) {
3736
4182
  void this.shutdown();
@@ -3890,6 +4336,7 @@ export class InteractiveMode {
3890
4336
  const nextActiveTools = this.getProfileToolNames(profile.name);
3891
4337
  this.session.setActiveToolsByName(nextActiveTools);
3892
4338
  this.session.setThinkingLevel(profile.thinkingLevel);
4339
+ this.session.setProfileName(profile.name);
3893
4340
  this.profilePromptSuffix = profile.systemPromptAppend || undefined;
3894
4341
  this.syncRuntimePromptSuffix();
3895
4342
  this.session.setIosmAutopilotEnabled(profile.name === "iosm");
@@ -3901,6 +4348,7 @@ export class InteractiveMode {
3901
4348
  this.updateEditorBorderColor();
3902
4349
  this.refreshBuiltInHeader();
3903
4350
  this.showStatus(`Profile: ${profile.name}`);
4351
+ this.showMetaModeProfileHint();
3904
4352
  }
3905
4353
  cycleProfile(direction) {
3906
4354
  if (this.session.isStreaming || this.session.isCompacting || this.iosmAutomationRun || this.iosmVerificationSession) {
@@ -4136,6 +4584,7 @@ export class InteractiveMode {
4136
4584
  const hasAutomationWork = this.iosmAutomationRun !== undefined;
4137
4585
  const verificationSession = this.iosmVerificationSession;
4138
4586
  const hasVerificationWork = verificationSession !== undefined;
4587
+ const hasSwarmWork = this.swarmActiveRunId !== undefined;
4139
4588
  const singularSession = this.singularAnalysisSession;
4140
4589
  const hasSingularWork = singularSession !== undefined;
4141
4590
  const hasMainStreaming = this.session.isStreaming;
@@ -4145,6 +4594,7 @@ export class InteractiveMode {
4145
4594
  if (!hasPendingQueuedMessages &&
4146
4595
  !hasAutomationWork &&
4147
4596
  !hasVerificationWork &&
4597
+ !hasSwarmWork &&
4148
4598
  !hasSingularWork &&
4149
4599
  !hasMainStreaming &&
4150
4600
  !hasRetryWork &&
@@ -4158,6 +4608,10 @@ export class InteractiveMode {
4158
4608
  if (this.iosmAutomationRun) {
4159
4609
  this.iosmAutomationRun.cancelRequested = true;
4160
4610
  }
4611
+ if (hasSwarmWork) {
4612
+ this.swarmStopRequested = true;
4613
+ this.swarmAbortController?.abort();
4614
+ }
4161
4615
  if (hasPendingQueuedMessages) {
4162
4616
  this.restoreQueuedMessagesToEditor();
4163
4617
  }
@@ -4178,9 +4632,11 @@ export class InteractiveMode {
4178
4632
  ? "Stopping IOSM automation..."
4179
4633
  : hasVerificationWork
4180
4634
  ? "Stopping IOSM verification..."
4181
- : hasSingularWork
4182
- ? "Stopping /singular analysis..."
4183
- : "Stopping current run...");
4635
+ : hasSwarmWork
4636
+ ? "Stopping swarm run..."
4637
+ : hasSingularWork
4638
+ ? "Stopping /singular analysis..."
4639
+ : "Stopping current run...");
4184
4640
  const abortPromises = [];
4185
4641
  if (hasMainStreaming) {
4186
4642
  abortPromises.push(this.session.abort());
@@ -7673,8 +8129,10 @@ export class InteractiveMode {
7673
8129
  });
7674
8130
  options.push("Close without decision");
7675
8131
  const selected = await this.showExtensionSelector("/singular: choose next step", options);
7676
- if (!selected || selected === "Close without decision")
8132
+ if (!selected || selected === "Close without decision") {
8133
+ this.showStatus("Singular: decision closed without execution.");
7677
8134
  return;
8135
+ }
7678
8136
  const match = selected.match(/^Option\s+(\d+)/);
7679
8137
  if (!match)
7680
8138
  return;
@@ -8655,7 +9113,7 @@ export class InteractiveMode {
8655
9113
  const mentionTask = cleaned.length > 0 ? cleaned : userInput;
8656
9114
  const orchestrationAwareAgent = /orchestrator/i.test(mentionedAgent);
8657
9115
  const mentionMode = orchestrationAwareAgent ? "parallel" : "sequential";
8658
- const mentionMaxParallel = orchestrationAwareAgent ? 20 : undefined;
9116
+ const mentionMaxParallel = orchestrationAwareAgent ? MAX_ORCHESTRATION_PARALLEL : undefined;
8659
9117
  const mentionPrompt = [
8660
9118
  `<orchestrate mode="${mentionMode}" agents="1"${mentionMaxParallel ? ` max_parallel="${mentionMaxParallel}"` : ""}>`,
8661
9119
  `- agent 1: profile=${this.activeProfileName} cwd=${this.sessionManager.getCwd()} agent=${mentionedAgent}`,
@@ -8666,8 +9124,8 @@ export class InteractiveMode {
8666
9124
  ...(orchestrationAwareAgent
8667
9125
  ? [
8668
9126
  "- Include delegate_parallel_hint in the task call.",
8669
- "- If user explicitly requested an agent count, set delegate_parallel_hint to that count (clamp 1..10).",
8670
- "- Otherwise set delegate_parallel_hint based on complexity: simple=1, medium=3-6, complex/risky=7-10.",
9127
+ `- If user explicitly requested an agent count, set delegate_parallel_hint to that count (clamp 1..${MAX_SUBAGENT_DELEGATE_PARALLEL}).`,
9128
+ `- Otherwise set delegate_parallel_hint based on complexity: simple=1, medium=3-6, complex/risky=7-${MAX_SUBAGENT_DELEGATE_PARALLEL}.`,
8671
9129
  "- For non-trivial tasks, prefer delegate_parallel_hint >= 2 and split into independent <delegate_task> workstreams.",
8672
9130
  '- Prefer existing custom agents for delegated work when suitable (use <delegate_task agent="name" ...>).',
8673
9131
  "- If no existing custom agent fits, create focused delegate streams via profile-based <delegate_task> blocks.",
@@ -8682,6 +9140,27 @@ export class InteractiveMode {
8682
9140
  });
8683
9141
  return;
8684
9142
  }
9143
+ if (this.activeProfileName === "meta") {
9144
+ await promptMetaWithParallelismGuard({
9145
+ session: this.session,
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
+ },
9161
+ });
9162
+ return;
9163
+ }
8685
9164
  await this.session.prompt(userInput);
8686
9165
  }
8687
9166
  createIosmVerificationEventBridge(options) {
@@ -8916,6 +9395,40 @@ export class InteractiveMode {
8916
9395
  command: commandParts.join(" "),
8917
9396
  };
8918
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
+ }
8919
9432
  isEffectiveContractReady(contract) {
8920
9433
  const hasText = (value) => typeof value === "string" && value.trim().length > 0;
8921
9434
  const hasList = (value) => Array.isArray(value) && value.some((item) => item.trim().length > 0);
@@ -9137,8 +9650,8 @@ export class InteractiveMode {
9137
9650
  if (token === "--max-parallel") {
9138
9651
  const next = rest[index + 1];
9139
9652
  const parsed = next ? Number.parseInt(next, 10) : Number.NaN;
9140
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
9141
- this.showWarning("Invalid --max-parallel value (expected 1..20).");
9653
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_ORCHESTRATION_PARALLEL) {
9654
+ this.showWarning(`Invalid --max-parallel value (expected 1..${MAX_ORCHESTRATION_PARALLEL}).`);
9142
9655
  return undefined;
9143
9656
  }
9144
9657
  maxParallel = parsed;
@@ -9252,12 +9765,15 @@ export class InteractiveMode {
9252
9765
  };
9253
9766
  }
9254
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";
9255
9773
  if (task.concurrency_class === "analysis")
9256
9774
  return "explore";
9257
9775
  if (task.concurrency_class === "verification" || task.concurrency_class === "tests")
9258
9776
  return "iosm_verifier";
9259
- if (task.concurrency_class === "docs")
9260
- return "plan";
9261
9777
  return "full";
9262
9778
  }
9263
9779
  estimateSwarmTaskCostUsd(task) {
@@ -9273,6 +9789,13 @@ export class InteractiveMode {
9273
9789
  const hasVeryComplexSignal = /(overhaul|major|system-wide|cross-cutting|multi-service|facet|registry)/i.test(brief) ||
9274
9790
  task.touches.length >= 5 ||
9275
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
+ }
9276
9799
  if (task.severity === "low" && task.touches.length <= 2 && task.scopes.length <= 2 && !hasComplexKeyword) {
9277
9800
  return 1;
9278
9801
  }
@@ -9287,6 +9810,38 @@ export class InteractiveMode {
9287
9810
  return 5;
9288
9811
  return hasComplexKeyword ? 3 : 1;
9289
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
+ }
9290
9845
  parseSwarmSpawnCandidates(output, parentTaskId) {
9291
9846
  const lines = output.split(/\r?\n/);
9292
9847
  const results = [];
@@ -9330,6 +9885,16 @@ export class InteractiveMode {
9330
9885
  }
9331
9886
  const profile = this.resolveSwarmTaskProfile(input.task);
9332
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;
9333
9898
  const safeDescription = input.task.brief.replace(/\s+/g, " ").trim().slice(0, 120).replace(/"/g, "'");
9334
9899
  const prompt = [
9335
9900
  `<swarm_task run_id="${input.meta.runId}" task_id="${input.task.id}" profile_hint="${profile}">`,
@@ -9342,10 +9907,15 @@ export class InteractiveMode {
9342
9907
  "",
9343
9908
  "Execution requirements:",
9344
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}".`,
9345
- `- Inside this single task call, prefer decomposition into <delegate_task> subtasks when delegate_parallel_hint >= 2 (target parallel fan-out up to ${delegateParallelHint}).`,
9346
- "- 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.",
9347
9916
  "- Keep edits inside declared scopes/touches. If scope expansion is required, explain and stop.",
9348
9917
  "- If blocked, respond with line: BLOCKED: <reason>",
9918
+ "- Return concise execution output; avoid long narrative if not needed.",
9349
9919
  "- Optional spawn candidates format: '- <description> | <path> | <change_type> | <low|medium|high>'",
9350
9920
  "</swarm_task>",
9351
9921
  ].join("\n");
@@ -9383,7 +9953,7 @@ export class InteractiveMode {
9383
9953
  resourceLoader,
9384
9954
  model,
9385
9955
  thinkingLevel: this.session.thinkingLevel,
9386
- profile: "full",
9956
+ profile: "meta",
9387
9957
  enableTaskTool: true,
9388
9958
  });
9389
9959
  swarmSession = created.session;
@@ -9400,6 +9970,14 @@ export class InteractiveMode {
9400
9970
  const taskErrors = [];
9401
9971
  let delegatedFailed = 0;
9402
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;
9403
9981
  const delegatedFailureCauses = new Map();
9404
9982
  const accumulateFailureCauses = (raw) => {
9405
9983
  if (!raw || typeof raw !== "object")
@@ -9413,13 +9991,71 @@ export class InteractiveMode {
9413
9991
  delegatedFailureCauses.set(cause, (delegatedFailureCauses.get(cause) ?? 0) + numeric);
9414
9992
  }
9415
9993
  };
9994
+ const emitProgress = (progress) => {
9995
+ input.onProgress?.({
9996
+ activeTool,
9997
+ toolCallsStarted,
9998
+ toolCallsCompleted,
9999
+ assistantMessages,
10000
+ delegatedTasks,
10001
+ delegatedFailed,
10002
+ ...progress,
10003
+ });
10004
+ };
9416
10005
  const chunks = [];
9417
10006
  const unsubscribe = swarmSession.subscribe((event) => {
9418
- if (event.type === "tool_execution_start" && event.toolName === "task") {
9419
- 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
+ }
9420
10052
  return;
9421
10053
  }
9422
10054
  if (event.type === "tool_execution_end" && event.toolName === "task") {
10055
+ toolCallsCompleted += 1;
10056
+ if (activeTool === event.toolName) {
10057
+ activeTool = undefined;
10058
+ }
9423
10059
  const result = event.result;
9424
10060
  const details = result?.details;
9425
10061
  if (typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)) {
@@ -9429,38 +10065,127 @@ export class InteractiveMode {
9429
10065
  delegatedTasks += Math.max(0, details.delegatedTasks);
9430
10066
  }
9431
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
+ });
9432
10081
  if (event.isError) {
9433
10082
  taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
9434
10083
  }
9435
10084
  return;
9436
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
+ }
9437
10097
  if (event.type === "message_end" && event.message.role === "assistant") {
10098
+ assistantMessages += 1;
9438
10099
  for (const part of event.message.content) {
9439
10100
  if (part.type === "text" && part.text.trim()) {
9440
10101
  chunks.push(part.text.trim());
9441
10102
  }
9442
10103
  }
10104
+ emitProgress({
10105
+ phase: "drafting response",
10106
+ phaseState: "responding",
10107
+ });
9443
10108
  }
9444
10109
  });
9445
10110
  try {
9446
- 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, {
9447
10125
  expandPromptTemplates: false,
9448
10126
  skipIosmAutopilot: true,
9449
10127
  skipOrchestrationDirective: true,
9450
10128
  source: "interactive",
9451
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]);
9452
10168
  }
9453
10169
  catch (error) {
9454
10170
  return {
9455
10171
  taskId: input.task.id,
9456
10172
  status: "error",
9457
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,
9458
10179
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9459
10180
  };
9460
10181
  }
9461
10182
  finally {
10183
+ detachStopListener?.();
10184
+ if (timeoutHandle) {
10185
+ clearTimeout(timeoutHandle);
10186
+ }
9462
10187
  unsubscribe();
9463
- if (swarmSession.isStreaming) {
10188
+ if (timedOut || swarmSession.isStreaming) {
9464
10189
  await swarmSession.abort().catch(() => {
9465
10190
  // best effort
9466
10191
  });
@@ -9482,6 +10207,7 @@ export class InteractiveMode {
9482
10207
  taskId: input.task.id,
9483
10208
  status: "error",
9484
10209
  error: "No task tool call executed by assistant.",
10210
+ failureCause: "protocol_violation",
9485
10211
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9486
10212
  };
9487
10213
  }
@@ -9523,6 +10249,7 @@ export class InteractiveMode {
9523
10249
  if (!input.resumeState) {
9524
10250
  store.init(input.meta, input.plan, initialState);
9525
10251
  }
10252
+ const swarmTaskById = new Map(input.plan.tasks.map((task) => [task.id, task]));
9526
10253
  let rollingIndex = input.projectIndex;
9527
10254
  let localStopRequested = false;
9528
10255
  const refreshIncrementalIndex = () => {
@@ -9540,52 +10267,159 @@ export class InteractiveMode {
9540
10267
  rollingIndex = rebuilt;
9541
10268
  };
9542
10269
  this.swarmActiveRunId = input.runId;
10270
+ this.swarmStopRequested = false;
10271
+ this.swarmAbortController = new AbortController();
10272
+ this.footerDataProvider.setSwarmBusy(true);
10273
+ this.footer.invalidate();
9543
10274
  this.showStatus(`Swarm run started: ${input.runId}`);
9544
- const schedulerResult = await runSwarmScheduler({
9545
- runId: input.runId,
9546
- plan: input.plan,
9547
- contract: input.contract,
9548
- maxParallel: input.meta.maxParallel,
9549
- budgetUsd: input.budgetUsd,
9550
- existingState: initialState,
9551
- dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
9552
- meta: input.meta,
9553
- task,
9554
- runtime,
10275
+ let schedulerResult;
10276
+ try {
10277
+ schedulerResult = await runSwarmScheduler({
10278
+ runId: input.runId,
10279
+ plan: input.plan,
9555
10280
  contract: input.contract,
9556
- }),
9557
- confirmSpawn: async ({ candidate, parentTask }) => {
9558
- const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
9559
- 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
+ }
9560
10319
  return true;
9561
- const choice = await this.showExtensionSelector([
9562
- `/swarm spawn candidate requires confirmation`,
9563
- `severity=${candidate.severity} task=${parentTask.id}`,
9564
- `description=${candidate.description}`,
9565
- `path=${candidate.path}`,
9566
- ].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
9567
- if (!choice || choice.startsWith("Reject")) {
9568
- this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
9569
- return false;
9570
- }
9571
- if (choice === "Abort run") {
9572
- localStopRequested = true;
9573
- this.showWarning("Swarm run marked to stop after current scheduling step.");
9574
- return false;
9575
- }
9576
- return true;
9577
- },
9578
- onEvent: (event) => {
9579
- store.appendEvent(event);
9580
- },
9581
- onStateChanged: (state) => {
9582
- store.saveState(state);
9583
- store.saveCheckpoint(state);
9584
- refreshIncrementalIndex();
9585
- },
9586
- shouldStop: () => this.shutdownRequested || localStopRequested,
9587
- });
9588
- 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
+ }
9589
10423
  const taskStates = Object.values(schedulerResult.state.tasks);
9590
10424
  const doneCount = taskStates.filter((task) => task.status === "done").length;
9591
10425
  const errorCount = taskStates.filter((task) => task.status === "error").length;
@@ -9641,6 +10475,8 @@ export class InteractiveMode {
9641
10475
  ].join("\n"));
9642
10476
  }
9643
10477
  async runSwarmFromTask(task, options) {
10478
+ if (!this.ensureSwarmModelReady("plain"))
10479
+ return;
9644
10480
  const contract = await this.ensureSwarmEffectiveContract(task);
9645
10481
  if (!contract)
9646
10482
  return;
@@ -9655,7 +10491,11 @@ export class InteractiveMode {
9655
10491
  index: indexInfo.index,
9656
10492
  });
9657
10493
  const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9658
- const maxParallel = Math.max(1, Math.min(20, options.maxParallel ?? 3));
10494
+ const maxParallel = this.resolveSwarmMaxParallel({
10495
+ requested: options.maxParallel,
10496
+ plan,
10497
+ source: "plain",
10498
+ });
9659
10499
  const meta = this.buildSwarmRunMeta({
9660
10500
  runId,
9661
10501
  source: "plain",
@@ -9677,6 +10517,8 @@ export class InteractiveMode {
9677
10517
  });
9678
10518
  }
9679
10519
  async runSwarmFromSingular(input) {
10520
+ if (!this.ensureSwarmModelReady("singular"))
10521
+ return;
9680
10522
  const analysis = this.loadSingularAnalysisByRunId(input.runId);
9681
10523
  if (!analysis) {
9682
10524
  this.showWarning(`Singular run not found: ${input.runId}`);
@@ -9699,7 +10541,11 @@ export class InteractiveMode {
9699
10541
  index: indexInfo.index,
9700
10542
  });
9701
10543
  const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9702
- const maxParallel = Math.max(1, Math.min(20, input.maxParallel ?? 3));
10544
+ const maxParallel = this.resolveSwarmMaxParallel({
10545
+ requested: input.maxParallel,
10546
+ plan,
10547
+ source: "singular",
10548
+ });
9703
10549
  const meta = this.buildSwarmRunMeta({
9704
10550
  runId,
9705
10551
  source: "singular",
@@ -9887,6 +10733,10 @@ export class InteractiveMode {
9887
10733
  });
9888
10734
  }
9889
10735
  async handleSwarmCommand(text) {
10736
+ if (this.swarmActiveRunId) {
10737
+ this.showWarning(`Swarm run already in progress: ${this.swarmActiveRunId}. Use /swarm watch.`);
10738
+ return;
10739
+ }
9890
10740
  if (this.session.isStreaming) {
9891
10741
  this.showWarning("Cannot run /swarm while the agent is processing another request.");
9892
10742
  return;
@@ -9969,8 +10819,8 @@ export class InteractiveMode {
9969
10819
  if (arg === "--agents") {
9970
10820
  const value = args[index + 1];
9971
10821
  const parsed = value ? Number.parseInt(value, 10) : Number.NaN;
9972
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
9973
- this.showWarning("Invalid --agents value (expected 1..20).");
10822
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_ORCHESTRATION_AGENTS) {
10823
+ this.showWarning(`Invalid --agents value (expected 1..${MAX_ORCHESTRATION_AGENTS}).`);
9974
10824
  return undefined;
9975
10825
  }
9976
10826
  agents = parsed;
@@ -9980,8 +10830,8 @@ export class InteractiveMode {
9980
10830
  if (arg === "--max-parallel") {
9981
10831
  const value = args[index + 1];
9982
10832
  const parsed = value ? Number.parseInt(value, 10) : Number.NaN;
9983
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
9984
- this.showWarning("Invalid --max-parallel value (expected 1..20).");
10833
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_ORCHESTRATION_PARALLEL) {
10834
+ this.showWarning(`Invalid --max-parallel value (expected 1..${MAX_ORCHESTRATION_PARALLEL}).`);
9985
10835
  return undefined;
9986
10836
  }
9987
10837
  maxParallel = parsed;
@@ -10100,6 +10950,9 @@ export class InteractiveMode {
10100
10950
  this.showWarning("--max-parallel is only valid with --parallel mode.");
10101
10951
  return undefined;
10102
10952
  }
10953
+ if (mode === "parallel" && maxParallel === undefined) {
10954
+ maxParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, agents));
10955
+ }
10103
10956
  if (maxParallel !== undefined && maxParallel > agents) {
10104
10957
  maxParallel = agents;
10105
10958
  }
@@ -10166,11 +11019,24 @@ export class InteractiveMode {
10166
11019
  const currentCwd = this.sessionManager.getCwd();
10167
11020
  const assignments = [];
10168
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 = [];
10169
11025
  for (let index = 0; index < parsed.agents; index++) {
10170
- const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? (this.activeProfileName || "full");
11026
+ const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? defaultAssignmentProfile;
10171
11027
  const assignmentCwd = parsed.cwds?.[index] ?? ".";
10172
11028
  const assignmentLock = parsed.locks?.[index];
10173
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);
10174
11040
  const resolvedCwd = path.resolve(currentCwd, assignmentCwd);
10175
11041
  assignmentRecords.push({
10176
11042
  profile: assignmentProfile,
@@ -10178,7 +11044,8 @@ export class InteractiveMode {
10178
11044
  lockKey: assignmentLock,
10179
11045
  dependsOn,
10180
11046
  });
10181
- 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
+ }`);
10182
11049
  }
10183
11050
  const teamRun = createTeamRun({
10184
11051
  cwd: currentCwd,
@@ -10191,8 +11058,16 @@ export class InteractiveMode {
10191
11058
  const runAssignments = teamRun.tasks.map((task, index) => `${assignments[index]} run_id=${teamRun.runId} task_id=${task.id}`);
10192
11059
  const taskCallHints = teamRun.tasks.map((task, index) => {
10193
11060
  const assignment = assignmentRecords[index];
10194
- 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}`;
10195
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
+ }
10196
11071
  this.chatContainer.addChild(new Spacer(1));
10197
11072
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "◆ ")) + theme.bold("/orchestrate") + theme.fg("muted", ` ${currentCwd}`), 1, 0));
10198
11073
  this.ui.requestRender();
@@ -10207,6 +11082,9 @@ export class InteractiveMode {
10207
11082
  "- for parallel mode, emit all independent task calls in one assistant response",
10208
11083
  "- in parallel mode, use parallel tool-call style (<use_parallel_tool_calls>)",
10209
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>",
10210
11088
  "- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
10211
11089
  "- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
10212
11090
  "- include run_id and task_id from each assignment in the task tool arguments",
@@ -10512,7 +11390,7 @@ export class InteractiveMode {
10512
11390
  "You are generating a custom IOSM subagent specification.",
10513
11391
  "Return ONLY a single JSON object and no additional text.",
10514
11392
  'Allowed JSON keys: name, description, profile, tools, disallowed_tools, system_prompt, cwd, background, instructions.',
10515
- 'profile must be one of: full, plan, iosm, explore, iosm_analyst, iosm_verifier, cycle_planner.',
11393
+ 'profile must be one of: full, plan, iosm, meta, explore, iosm_analyst, iosm_verifier, cycle_planner.',
10516
11394
  "name must be short snake/kebab case, suitable for @mention.",
10517
11395
  "tools/disallowed_tools must be arrays of tool names when present.",
10518
11396
  "instructions must be a concise markdown body for the agent file.",
@@ -12194,6 +13072,10 @@ The agent will automatically receive IOSM context on every turn.`;
12194
13072
  }
12195
13073
  }
12196
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
+ }
12197
13079
  const extensionRunner = this.session.extensionRunner;
12198
13080
  // Emit user_bash event to let extensions intercept
12199
13081
  const eventResult = extensionRunner