iosm-cli 0.2.4 → 0.2.6

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 (66) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +447 -285
  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 +16 -2
  23. package/dist/core/shared-memory.d.ts.map +1 -1
  24. package/dist/core/shared-memory.js +283 -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 +79 -11
  48. package/dist/core/tools/shared-memory.js.map +1 -1
  49. package/dist/core/tools/task.d.ts +12 -1
  50. package/dist/core/tools/task.d.ts.map +1 -1
  51. package/dist/core/tools/task.js +1023 -76
  52. package/dist/core/tools/task.js.map +1 -1
  53. package/dist/core/tools/yq.d.ts.map +1 -1
  54. package/dist/core/tools/yq.js +2 -0
  55. package/dist/core/tools/yq.js.map +1 -1
  56. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/footer.js +2 -1
  58. package/dist/modes/interactive/components/footer.js.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  60. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  61. package/dist/modes/interactive/interactive-mode.js +881 -74
  62. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  63. package/docs/cli-reference.md +4 -0
  64. package/docs/interactive-mode.md +2 -0
  65. package/docs/orchestration-and-subagents.md +5 -0
  66. 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";
@@ -23,6 +23,7 @@ import { resolveModelScope } from "../../core/model-resolver.js";
23
23
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
24
24
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
25
25
  import { ContractService, normalizeEngineeringContract, } from "../../core/contract.js";
26
+ import { readSharedMemory } from "../../core/shared-memory.js";
26
27
  import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadProjectIndex, queryProjectIndex, saveProjectIndex, } from "../../core/project-index/index.js";
27
28
  import { SingularService, } from "../../core/singular.js";
28
29
  import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
@@ -88,8 +89,8 @@ function parseRequestedParallelAgentCount(text) {
88
89
  const patterns = [
89
90
  /(\d+)\s+(?:parallel|concurrent)\s+agents?/i,
90
91
  /(\d+)\s+agents?/i,
91
- /(\d+)\s+паралл\w*\s+агент\w*/i,
92
- /(\d+)\s+агент\w*/i,
92
+ /(\d+)\s+паралл[\p{L}\p{N}_-]*\s+агент[\p{L}\p{N}_-]*/iu,
93
+ /(\d+)\s+агент[\p{L}\p{N}_-]*/iu,
93
94
  ];
94
95
  for (const pattern of patterns) {
95
96
  const match = text.match(pattern);
@@ -127,6 +128,12 @@ function buildMetaParallelismCorrection(input) {
127
128
  `Meta runtime correction: this run currently has ${input.launchedTopLevelTasks} top-level task call(s), but it should have at least ${input.requiredTopLevelTasks}.`,
128
129
  "Stop manual sequential execution in the main agent and convert the execution graph into parallel task calls now.",
129
130
  "Emit the missing top-level task calls in the same assistant response when branches are independent.",
131
+ input.workerDiversityMissing
132
+ ? `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.`
133
+ : undefined,
134
+ input.nestedDelegationMissing
135
+ ? "Top-level fan-out exists, but no nested delegates were observed. For each broad top-level stream, emit nested <delegate_task> fan-out or one explicit line: DELEGATION_IMPOSSIBLE: <precise reason>."
136
+ : undefined,
130
137
  "Each task tool call MUST include description and prompt.",
131
138
  'If you want a custom subagent, pass it via agent="name"; keep profile set to the capability profile, not the custom subagent name.',
132
139
  input.rawRootDelegateBlocks && input.rawRootDelegateBlocks > 0
@@ -145,25 +152,44 @@ function buildMetaParallelismCorrection(input) {
145
152
  }
146
153
  async function promptMetaWithParallelismGuard(input) {
147
154
  let taskToolCalls = 0;
155
+ let completedTaskToolCalls = 0;
148
156
  let nonTaskToolCalls = 0;
149
157
  let taskPlanSnapshot;
150
158
  let taskToolError;
151
159
  let rawRootDelegateBlocks = 0;
152
160
  let delegationImpossibleDeclared = false;
153
161
  let correctionText;
154
- const maybeScheduleCorrection = () => {
162
+ let distinctTaskWorkers = new Set();
163
+ let delegatedChildTasksSeen = 0;
164
+ const maybeScheduleCorrection = (options) => {
155
165
  const requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
156
- if (!requiredTopLevelTasks || taskToolCalls >= requiredTopLevelTasks || delegationImpossibleDeclared) {
166
+ if (!requiredTopLevelTasks || delegationImpossibleDeclared) {
167
+ return;
168
+ }
169
+ const canAssessNestedDelegation = options?.finalize || completedTaskToolCalls >= taskToolCalls;
170
+ const needsMoreTopLevelTasks = taskToolCalls < requiredTopLevelTasks;
171
+ const workerDiversityMissing = !needsMoreTopLevelTasks &&
172
+ requiredTopLevelTasks >= 2 &&
173
+ distinctTaskWorkers.size === 1 &&
174
+ delegatedChildTasksSeen === 0;
175
+ const nestedDelegationMissing = !needsMoreTopLevelTasks &&
176
+ !workerDiversityMissing &&
177
+ requiredTopLevelTasks >= 2 &&
178
+ taskToolCalls > 0 &&
179
+ delegatedChildTasksSeen === 0 &&
180
+ canAssessNestedDelegation &&
181
+ !taskToolError;
182
+ if (!needsMoreTopLevelTasks && !workerDiversityMissing && !nestedDelegationMissing) {
157
183
  return;
158
184
  }
159
185
  const hasComplexPlan = !!taskPlanSnapshot;
160
186
  const hasTaskFailure = !!taskToolError;
161
187
  const hasRawRootDelegates = rawRootDelegateBlocks > 0;
162
- if (!hasTaskFailure && !hasRawRootDelegates && nonTaskToolCalls < 1) {
163
- return;
164
- }
165
- if (!hasComplexPlan && !hasTaskFailure && !hasRawRootDelegates && nonTaskToolCalls < 2) {
166
- return;
188
+ if (needsMoreTopLevelTasks && !hasTaskFailure && !hasRawRootDelegates && !nestedDelegationMissing) {
189
+ const minimumNonTaskCalls = hasComplexPlan ? 0 : 2;
190
+ if (!options?.finalize && nonTaskToolCalls < minimumNonTaskCalls) {
191
+ return;
192
+ }
167
193
  }
168
194
  correctionText = buildMetaParallelismCorrection({
169
195
  requiredTopLevelTasks,
@@ -171,6 +197,9 @@ async function promptMetaWithParallelismGuard(input) {
171
197
  taskPlanSnapshot,
172
198
  taskToolError,
173
199
  rawRootDelegateBlocks,
200
+ workerDiversityMissing,
201
+ distinctWorkers: distinctTaskWorkers.size,
202
+ nestedDelegationMissing,
174
203
  });
175
204
  };
176
205
  const unsubscribe = input.session.subscribe((event) => {
@@ -192,6 +221,17 @@ async function promptMetaWithParallelismGuard(input) {
192
221
  if (event.type === "tool_execution_start") {
193
222
  if (event.toolName === "task") {
194
223
  taskToolCalls += 1;
224
+ const args = event.args && typeof event.args === "object" ? event.args : undefined;
225
+ const workerIdentityRaw = (typeof args?.agent === "string" && args.agent.trim()) ||
226
+ (typeof args?.profile === "string" && args.profile.trim()) ||
227
+ undefined;
228
+ if (workerIdentityRaw) {
229
+ distinctTaskWorkers.add(workerIdentityRaw.toLowerCase());
230
+ }
231
+ else if (args) {
232
+ // Distinguish "explicit task call with omitted identity" from missing event payload.
233
+ distinctTaskWorkers.add("__default_profile__");
234
+ }
195
235
  }
196
236
  else {
197
237
  nonTaskToolCalls += 1;
@@ -200,22 +240,76 @@ async function promptMetaWithParallelismGuard(input) {
200
240
  return;
201
241
  }
202
242
  if (event.type === "tool_execution_end" && event.toolName === "task" && event.isError) {
243
+ completedTaskToolCalls += 1;
203
244
  taskToolError = extractTaskToolErrorText(event.result) ?? taskToolError;
204
245
  maybeScheduleCorrection();
246
+ return;
247
+ }
248
+ if (event.type === "tool_execution_end" && event.toolName === "task" && !event.isError) {
249
+ completedTaskToolCalls += 1;
250
+ const result = event.result;
251
+ const delegatedTasks = result?.details?.delegatedTasks;
252
+ if (typeof delegatedTasks === "number" && Number.isFinite(delegatedTasks) && delegatedTasks > 0) {
253
+ delegatedChildTasksSeen += delegatedTasks;
254
+ }
255
+ maybeScheduleCorrection();
205
256
  }
206
257
  });
207
258
  try {
208
259
  await input.session.prompt(input.userInput);
209
- const requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
260
+ maybeScheduleCorrection({ finalize: true });
261
+ let requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
262
+ let workerDiversityMissingAfterRun = !!requiredTopLevelTasks &&
263
+ requiredTopLevelTasks >= 2 &&
264
+ distinctTaskWorkers.size === 1 &&
265
+ delegatedChildTasksSeen === 0;
266
+ let nestedDelegationMissingAfterRun = !!requiredTopLevelTasks &&
267
+ requiredTopLevelTasks >= 2 &&
268
+ taskToolCalls >= requiredTopLevelTasks &&
269
+ taskToolCalls > 0 &&
270
+ delegatedChildTasksSeen === 0 &&
271
+ !workerDiversityMissingAfterRun &&
272
+ !taskToolError;
210
273
  if (correctionText &&
211
274
  requiredTopLevelTasks &&
212
- taskToolCalls < requiredTopLevelTasks &&
275
+ (taskToolCalls < requiredTopLevelTasks || workerDiversityMissingAfterRun || nestedDelegationMissingAfterRun) &&
213
276
  !delegationImpossibleDeclared) {
214
277
  await input.session.prompt(correctionText, {
215
278
  expandPromptTemplates: false,
216
279
  skipOrchestrationDirective: true,
217
280
  source: "interactive",
218
281
  });
282
+ maybeScheduleCorrection({ finalize: true });
283
+ requiredTopLevelTasks = deriveMetaRequiredTopLevelTaskCalls(input.userInput, taskPlanSnapshot);
284
+ workerDiversityMissingAfterRun =
285
+ !!requiredTopLevelTasks &&
286
+ requiredTopLevelTasks >= 2 &&
287
+ distinctTaskWorkers.size === 1 &&
288
+ delegatedChildTasksSeen === 0;
289
+ nestedDelegationMissingAfterRun =
290
+ !!requiredTopLevelTasks &&
291
+ requiredTopLevelTasks >= 2 &&
292
+ taskToolCalls >= requiredTopLevelTasks &&
293
+ taskToolCalls > 0 &&
294
+ delegatedChildTasksSeen === 0 &&
295
+ !workerDiversityMissingAfterRun &&
296
+ !taskToolError;
297
+ }
298
+ if (requiredTopLevelTasks &&
299
+ (taskToolCalls < requiredTopLevelTasks ||
300
+ workerDiversityMissingAfterRun ||
301
+ nestedDelegationMissingAfterRun) &&
302
+ !delegationImpossibleDeclared &&
303
+ input.onPersistentNonCompliance) {
304
+ await input.onPersistentNonCompliance({
305
+ requiredTopLevelTasks,
306
+ launchedTopLevelTasks: taskToolCalls,
307
+ distinctWorkers: distinctTaskWorkers.size,
308
+ workerDiversityMissing: workerDiversityMissingAfterRun,
309
+ nestedDelegationMissing: nestedDelegationMissingAfterRun,
310
+ taskPlanSnapshot,
311
+ taskToolError,
312
+ });
219
313
  }
220
314
  }
221
315
  finally {
@@ -549,6 +643,8 @@ export class InteractiveMode {
549
643
  this.sessionAllowedToolSignatures = new Set();
550
644
  this.singularLastEffectiveContract = {};
551
645
  this.swarmActiveRunId = undefined;
646
+ this.swarmStopRequested = false;
647
+ this.swarmAbortController = undefined;
552
648
  this.lastSigintTime = 0;
553
649
  this.lastEscapeTime = 0;
554
650
  this.changelogMarkdown = undefined;
@@ -559,6 +655,7 @@ export class InteractiveMode {
559
655
  this.streamingComponent = undefined;
560
656
  this.streamingMessage = undefined;
561
657
  this.currentTurnSawAssistantMessage = false;
658
+ this.currentTurnSawTaskToolCall = false;
562
659
  // Tool execution tracking: toolCallId -> component
563
660
  this.pendingTools = new Map();
564
661
  // Subagent execution tracking with live progress/metadata for task tool calls.
@@ -737,6 +834,17 @@ export class InteractiveMode {
737
834
  }
738
835
  }
739
836
  async requestToolPermission(request) {
837
+ if (this.activeProfileName === "meta" &&
838
+ !this.currentTurnSawTaskToolCall &&
839
+ (request.toolName === "bash" || request.toolName === "edit" || request.toolName === "write")) {
840
+ 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.`);
841
+ return false;
842
+ }
843
+ if (isReadOnlyProfileName(this.activeProfileName) &&
844
+ (request.toolName === "bash" || request.toolName === "edit" || request.toolName === "write")) {
845
+ this.showWarning(`Tool ${request.toolName} is disabled in ${this.activeProfileName} profile. Switch to full/meta/iosm for mutating operations.`);
846
+ return false;
847
+ }
740
848
  for (const rule of this.permissionDenyRules) {
741
849
  if (this.matchesPermissionRule(rule, request)) {
742
850
  this.showWarning(`Denied by rule: ${rule}`);
@@ -2138,7 +2246,7 @@ export class InteractiveMode {
2138
2246
  sessionManager: this.sessionManager,
2139
2247
  modelRegistry: this.session.modelRegistry,
2140
2248
  model: this.session.model,
2141
- isIdle: () => !this.session.isStreaming,
2249
+ isIdle: () => !this.session.isStreaming && this.swarmActiveRunId === undefined,
2142
2250
  abort: () => this.session.abort(),
2143
2251
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
2144
2252
  shutdown: () => {
@@ -2710,6 +2818,7 @@ export class InteractiveMode {
2710
2818
  this.session.isRetrying ||
2711
2819
  this.iosmAutomationRun !== undefined ||
2712
2820
  this.iosmVerificationSession !== undefined ||
2821
+ this.swarmActiveRunId !== undefined ||
2713
2822
  this.singularAnalysisSession !== undefined ||
2714
2823
  queuedMessages.steering.length > 0 ||
2715
2824
  queuedMessages.followUp.length > 0 ||
@@ -3116,6 +3225,173 @@ export class InteractiveMode {
3116
3225
  durationMs: Date.now() - subagent.startTime,
3117
3226
  });
3118
3227
  }
3228
+ getSwarmSubagentKey(runId, taskId) {
3229
+ return `swarm:${runId}:${taskId}`;
3230
+ }
3231
+ ensureSwarmSubagentDisplay(input) {
3232
+ const key = this.getSwarmSubagentKey(input.runId, input.taskId);
3233
+ const existing = this.subagentComponents.get(key);
3234
+ if (existing) {
3235
+ return existing;
3236
+ }
3237
+ const profile = (input.profile?.trim() || this.resolveSwarmTaskProfile(input.task)).trim();
3238
+ const info = {
3239
+ description: input.task.brief || input.task.id,
3240
+ profile,
3241
+ status: "running",
3242
+ phase: "starting subagent",
3243
+ phaseState: "starting",
3244
+ cwd: this.sessionManager.getCwd(),
3245
+ toolCallsStarted: 0,
3246
+ toolCallsCompleted: 0,
3247
+ assistantMessages: 0,
3248
+ delegatedTasks: 0,
3249
+ delegatedSucceeded: 0,
3250
+ delegatedFailed: 0,
3251
+ };
3252
+ const component = new SubagentMessageComponent(info);
3253
+ this.chatContainer.addChild(component);
3254
+ const state = {
3255
+ component,
3256
+ startTime: Date.now(),
3257
+ profile: info.profile,
3258
+ description: info.description,
3259
+ cwd: info.cwd,
3260
+ agent: info.agent,
3261
+ lockKey: info.lockKey,
3262
+ isolation: info.isolation,
3263
+ phase: info.phase,
3264
+ phaseState: info.phaseState,
3265
+ activeTool: info.activeTool,
3266
+ toolCallsStarted: info.toolCallsStarted ?? 0,
3267
+ toolCallsCompleted: info.toolCallsCompleted ?? 0,
3268
+ assistantMessages: info.assistantMessages ?? 0,
3269
+ delegatedTasks: info.delegatedTasks,
3270
+ delegatedSucceeded: info.delegatedSucceeded,
3271
+ delegatedFailed: info.delegatedFailed,
3272
+ delegateIndex: info.delegateIndex,
3273
+ delegateTotal: info.delegateTotal,
3274
+ delegateDescription: info.delegateDescription,
3275
+ delegateProfile: info.delegateProfile,
3276
+ delegateItems: info.delegateItems,
3277
+ };
3278
+ this.subagentComponents.set(key, state);
3279
+ this.ensureSubagentElapsedTimer();
3280
+ this.ui.requestRender();
3281
+ return state;
3282
+ }
3283
+ updateSwarmSubagentProgress(input) {
3284
+ const state = this.ensureSwarmSubagentDisplay({
3285
+ runId: input.runId,
3286
+ taskId: input.taskId,
3287
+ task: input.task,
3288
+ profile: input.profile,
3289
+ });
3290
+ const progress = input.progress;
3291
+ if (typeof progress.phase === "string" && progress.phase.trim()) {
3292
+ state.phase = progress.phase.trim();
3293
+ }
3294
+ if (isSubagentPhaseState(progress.phaseState)) {
3295
+ state.phaseState = progress.phaseState;
3296
+ }
3297
+ if (typeof progress.cwd === "string" && progress.cwd.trim()) {
3298
+ state.cwd = progress.cwd.trim();
3299
+ }
3300
+ if ("activeTool" in progress) {
3301
+ state.activeTool =
3302
+ typeof progress.activeTool === "string" && progress.activeTool.trim().length > 0
3303
+ ? progress.activeTool.trim()
3304
+ : undefined;
3305
+ }
3306
+ if (typeof progress.toolCallsStarted === "number" && Number.isFinite(progress.toolCallsStarted)) {
3307
+ state.toolCallsStarted = Math.max(0, progress.toolCallsStarted);
3308
+ }
3309
+ if (typeof progress.toolCallsCompleted === "number" && Number.isFinite(progress.toolCallsCompleted)) {
3310
+ state.toolCallsCompleted = Math.max(0, progress.toolCallsCompleted);
3311
+ }
3312
+ if (typeof progress.assistantMessages === "number" && Number.isFinite(progress.assistantMessages)) {
3313
+ state.assistantMessages = Math.max(0, progress.assistantMessages);
3314
+ }
3315
+ if (typeof progress.delegatedTasks === "number" && Number.isFinite(progress.delegatedTasks)) {
3316
+ state.delegatedTasks = Math.max(0, progress.delegatedTasks);
3317
+ }
3318
+ if (typeof progress.delegatedSucceeded === "number" && Number.isFinite(progress.delegatedSucceeded)) {
3319
+ state.delegatedSucceeded = Math.max(0, progress.delegatedSucceeded);
3320
+ }
3321
+ if (typeof progress.delegatedFailed === "number" && Number.isFinite(progress.delegatedFailed)) {
3322
+ state.delegatedFailed = Math.max(0, progress.delegatedFailed);
3323
+ }
3324
+ if ("delegateIndex" in progress) {
3325
+ state.delegateIndex =
3326
+ typeof progress.delegateIndex === "number" && progress.delegateIndex > 0
3327
+ ? Math.floor(progress.delegateIndex)
3328
+ : undefined;
3329
+ }
3330
+ if ("delegateTotal" in progress) {
3331
+ state.delegateTotal =
3332
+ typeof progress.delegateTotal === "number" && progress.delegateTotal > 0
3333
+ ? Math.floor(progress.delegateTotal)
3334
+ : undefined;
3335
+ }
3336
+ if ("delegateDescription" in progress) {
3337
+ state.delegateDescription =
3338
+ typeof progress.delegateDescription === "string" && progress.delegateDescription.trim().length > 0
3339
+ ? progress.delegateDescription.trim()
3340
+ : undefined;
3341
+ }
3342
+ if ("delegateProfile" in progress) {
3343
+ state.delegateProfile =
3344
+ typeof progress.delegateProfile === "string" && progress.delegateProfile.trim().length > 0
3345
+ ? progress.delegateProfile.trim()
3346
+ : undefined;
3347
+ }
3348
+ if ("delegateItems" in progress) {
3349
+ state.delegateItems = Array.isArray(progress.delegateItems) ? progress.delegateItems : undefined;
3350
+ }
3351
+ this.updateRunningSubagentDisplay(state);
3352
+ this.ui.requestRender();
3353
+ }
3354
+ finalizeSwarmSubagentDisplay(input) {
3355
+ const key = this.getSwarmSubagentKey(input.runId, input.taskId);
3356
+ const state = this.subagentComponents.get(key);
3357
+ if (!state)
3358
+ return;
3359
+ const durationMs = Date.now() - state.startTime;
3360
+ state.component.update({
3361
+ description: state.description,
3362
+ profile: state.profile,
3363
+ status: input.status,
3364
+ durationMs,
3365
+ phaseState: state.phaseState,
3366
+ cwd: state.cwd,
3367
+ agent: state.agent,
3368
+ lockKey: state.lockKey,
3369
+ isolation: state.isolation,
3370
+ toolCallsStarted: state.toolCallsStarted,
3371
+ toolCallsCompleted: state.toolCallsCompleted,
3372
+ assistantMessages: state.assistantMessages,
3373
+ delegatedTasks: state.delegatedTasks,
3374
+ delegatedSucceeded: state.delegatedSucceeded,
3375
+ delegatedFailed: state.delegatedFailed,
3376
+ errorMessage: input.status === "error" ? input.errorMessage ?? "error" : undefined,
3377
+ });
3378
+ this.subagentComponents.delete(key);
3379
+ this.stopSubagentElapsedTimerIfIdle();
3380
+ this.ui.requestRender();
3381
+ }
3382
+ finalizeSwarmRunSubagentDisplays(runId, errorMessage) {
3383
+ for (const [key] of this.subagentComponents.entries()) {
3384
+ if (!key.startsWith(`swarm:${runId}:`))
3385
+ continue;
3386
+ const taskId = key.slice(`swarm:${runId}:`.length);
3387
+ this.finalizeSwarmSubagentDisplay({
3388
+ runId,
3389
+ taskId,
3390
+ status: "error",
3391
+ errorMessage,
3392
+ });
3393
+ }
3394
+ }
3119
3395
  ensureSubagentElapsedTimer() {
3120
3396
  if (this.subagentElapsedTimer || this.subagentComponents.size === 0) {
3121
3397
  return;
@@ -3156,6 +3432,7 @@ export class InteractiveMode {
3156
3432
  switch (event.type) {
3157
3433
  case "agent_start":
3158
3434
  this.currentTurnSawAssistantMessage = false;
3435
+ this.currentTurnSawTaskToolCall = false;
3159
3436
  // Restore main escape handler if retry handler is still active
3160
3437
  // (retry success event fires later, but we need main handler now)
3161
3438
  if (this.retryEscapeHandler) {
@@ -3286,6 +3563,9 @@ export class InteractiveMode {
3286
3563
  this.ui.requestRender();
3287
3564
  break;
3288
3565
  case "tool_execution_start": {
3566
+ if (event.toolName === "task") {
3567
+ this.currentTurnSawTaskToolCall = true;
3568
+ }
3289
3569
  if (event.toolName === "task" && !this.subagentComponents.has(event.toolCallId)) {
3290
3570
  const staleTaskComponent = this.pendingTools.get(event.toolCallId);
3291
3571
  if (staleTaskComponent) {
@@ -3922,6 +4202,15 @@ export class InteractiveMode {
3922
4202
  // =========================================================================
3923
4203
  handleCtrlC() {
3924
4204
  const now = Date.now();
4205
+ if (this.swarmActiveRunId) {
4206
+ if (this.swarmStopRequested && now - this.lastSigintTime < 500) {
4207
+ void this.shutdown();
4208
+ return;
4209
+ }
4210
+ this.lastSigintTime = now;
4211
+ void this.interruptCurrentWork();
4212
+ return;
4213
+ }
3925
4214
  if (this.iosmAutomationRun) {
3926
4215
  if (this.iosmAutomationRun.cancelRequested && now - this.lastSigintTime < 500) {
3927
4216
  void this.shutdown();
@@ -4329,6 +4618,7 @@ export class InteractiveMode {
4329
4618
  const hasAutomationWork = this.iosmAutomationRun !== undefined;
4330
4619
  const verificationSession = this.iosmVerificationSession;
4331
4620
  const hasVerificationWork = verificationSession !== undefined;
4621
+ const hasSwarmWork = this.swarmActiveRunId !== undefined;
4332
4622
  const singularSession = this.singularAnalysisSession;
4333
4623
  const hasSingularWork = singularSession !== undefined;
4334
4624
  const hasMainStreaming = this.session.isStreaming;
@@ -4338,6 +4628,7 @@ export class InteractiveMode {
4338
4628
  if (!hasPendingQueuedMessages &&
4339
4629
  !hasAutomationWork &&
4340
4630
  !hasVerificationWork &&
4631
+ !hasSwarmWork &&
4341
4632
  !hasSingularWork &&
4342
4633
  !hasMainStreaming &&
4343
4634
  !hasRetryWork &&
@@ -4351,6 +4642,10 @@ export class InteractiveMode {
4351
4642
  if (this.iosmAutomationRun) {
4352
4643
  this.iosmAutomationRun.cancelRequested = true;
4353
4644
  }
4645
+ if (hasSwarmWork) {
4646
+ this.swarmStopRequested = true;
4647
+ this.swarmAbortController?.abort();
4648
+ }
4354
4649
  if (hasPendingQueuedMessages) {
4355
4650
  this.restoreQueuedMessagesToEditor();
4356
4651
  }
@@ -4371,9 +4666,11 @@ export class InteractiveMode {
4371
4666
  ? "Stopping IOSM automation..."
4372
4667
  : hasVerificationWork
4373
4668
  ? "Stopping IOSM verification..."
4374
- : hasSingularWork
4375
- ? "Stopping /singular analysis..."
4376
- : "Stopping current run...");
4669
+ : hasSwarmWork
4670
+ ? "Stopping swarm run..."
4671
+ : hasSingularWork
4672
+ ? "Stopping /singular analysis..."
4673
+ : "Stopping current run...");
4377
4674
  const abortPromises = [];
4378
4675
  if (hasMainStreaming) {
4379
4676
  abortPromises.push(this.session.abort());
@@ -7866,8 +8163,10 @@ export class InteractiveMode {
7866
8163
  });
7867
8164
  options.push("Close without decision");
7868
8165
  const selected = await this.showExtensionSelector("/singular: choose next step", options);
7869
- if (!selected || selected === "Close without decision")
8166
+ if (!selected || selected === "Close without decision") {
8167
+ this.showStatus("Singular: decision closed without execution.");
7870
8168
  return;
8169
+ }
7871
8170
  const match = selected.match(/^Option\s+(\d+)/);
7872
8171
  if (!match)
7873
8172
  return;
@@ -8879,6 +9178,26 @@ export class InteractiveMode {
8879
9178
  await promptMetaWithParallelismGuard({
8880
9179
  session: this.session,
8881
9180
  userInput,
9181
+ onPersistentNonCompliance: async (details) => {
9182
+ if (typeof this.runSwarmFromTask !== "function")
9183
+ return;
9184
+ if (this.session.isStreaming || this.iosmAutomationRun || this.iosmVerificationSession)
9185
+ return;
9186
+ const topLevelSatisfied = details.launchedTopLevelTasks >= details.requiredTopLevelTasks;
9187
+ if (details.nestedDelegationMissing && topLevelSatisfied && !details.workerDiversityMissing) {
9188
+ this.showWarning("META quality warning: top-level fan-out completed, but nested delegate fan-out was not observed. " +
9189
+ "Repeat with explicit nested delegation requirements or include DELEGATION_IMPOSSIBLE for narrow streams.");
9190
+ return;
9191
+ }
9192
+ const explicitRequested = parseRequestedParallelAgentCount(userInput);
9193
+ const hasComplexSignal = /\b(audit|security|hardening|refactor|migration|orchestrat|parallel|delegate|multi[-\s]?agent)\b/i.test(userInput);
9194
+ const fallbackParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, explicitRequested ??
9195
+ (hasComplexSignal
9196
+ ? Math.max(details.requiredTopLevelTasks, 6)
9197
+ : Math.max(details.requiredTopLevelTasks, 3))));
9198
+ this.showWarning(`META enforcement fallback: orchestration contract not satisfied (${details.launchedTopLevelTasks}/${details.requiredTopLevelTasks} task calls). Launching /swarm run.`);
9199
+ await this.runSwarmFromTask(userInput, { maxParallel: fallbackParallel });
9200
+ },
8882
9201
  });
8883
9202
  return;
8884
9203
  }
@@ -9116,6 +9435,40 @@ export class InteractiveMode {
9116
9435
  command: commandParts.join(" "),
9117
9436
  };
9118
9437
  }
9438
+ resolveOrchestrateDefaultAssignmentProfile(parsed) {
9439
+ const active = this.activeProfileName || "full";
9440
+ if (parsed.mode !== "parallel")
9441
+ return active;
9442
+ if (parsed.profile || (parsed.profiles && parsed.profiles.length > 0))
9443
+ return active;
9444
+ if (isReadOnlyProfileName(active))
9445
+ return active;
9446
+ return "meta";
9447
+ }
9448
+ deriveOrchestrateDelegateParallelHint(input) {
9449
+ const normalizedTask = input.task.toLowerCase();
9450
+ const highRisk = /\b(refactor|rewrite|migration|migrate|breaking|rollback|security|auth|authentication|authorization|permission|payment|billing|schema|database|critical)\b/i.test(normalizedTask);
9451
+ const mediumRisk = /\b(cross[-\s]?module|architecture|infra|platform|multi[-\s]?file|integration|audit|hardening)\b/i.test(normalizedTask);
9452
+ let hint = input.mode === "parallel"
9453
+ ? Math.max(2, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, input.maxParallel ?? input.agents))
9454
+ : 1;
9455
+ if (highRisk) {
9456
+ hint = Math.max(hint, 7);
9457
+ }
9458
+ else if (mediumRisk) {
9459
+ hint = Math.max(hint, 5);
9460
+ }
9461
+ if (input.mode === "parallel" && input.dependencyEdges >= input.agents) {
9462
+ hint = Math.max(hint, 6);
9463
+ }
9464
+ if (input.hasDependencies) {
9465
+ hint = Math.max(2, Math.min(hint, 6));
9466
+ }
9467
+ if (input.hasLock) {
9468
+ hint = Math.max(1, Math.min(hint, 4));
9469
+ }
9470
+ return Math.max(1, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, hint));
9471
+ }
9119
9472
  isEffectiveContractReady(contract) {
9120
9473
  const hasText = (value) => typeof value === "string" && value.trim().length > 0;
9121
9474
  const hasList = (value) => Array.isArray(value) && value.some((item) => item.trim().length > 0);
@@ -9452,12 +9805,15 @@ export class InteractiveMode {
9452
9805
  };
9453
9806
  }
9454
9807
  resolveSwarmTaskProfile(task) {
9808
+ const hint = this.deriveSwarmTaskDelegateParallelHint(task);
9809
+ if (task.concurrency_class === "docs")
9810
+ return "plan";
9811
+ if (hint >= 2)
9812
+ return "meta";
9455
9813
  if (task.concurrency_class === "analysis")
9456
9814
  return "explore";
9457
9815
  if (task.concurrency_class === "verification" || task.concurrency_class === "tests")
9458
9816
  return "iosm_verifier";
9459
- if (task.concurrency_class === "docs")
9460
- return "plan";
9461
9817
  return "full";
9462
9818
  }
9463
9819
  estimateSwarmTaskCostUsd(task) {
@@ -9473,6 +9829,13 @@ export class InteractiveMode {
9473
9829
  const hasVeryComplexSignal = /(overhaul|major|system-wide|cross-cutting|multi-service|facet|registry)/i.test(brief) ||
9474
9830
  task.touches.length >= 5 ||
9475
9831
  task.scopes.length >= 4;
9832
+ if (task.concurrency_class === "analysis" || task.concurrency_class === "docs") {
9833
+ if (task.severity === "low")
9834
+ return 1;
9835
+ if (task.touches.length >= 6 || task.scopes.length >= 4 || hasComplexKeyword)
9836
+ return 2;
9837
+ return 1;
9838
+ }
9476
9839
  if (task.severity === "low" && task.touches.length <= 2 && task.scopes.length <= 2 && !hasComplexKeyword) {
9477
9840
  return 1;
9478
9841
  }
@@ -9487,6 +9850,38 @@ export class InteractiveMode {
9487
9850
  return 5;
9488
9851
  return hasComplexKeyword ? 3 : 1;
9489
9852
  }
9853
+ ensureSwarmModelReady(source) {
9854
+ if (this.session.model)
9855
+ return true;
9856
+ if (source === "singular") {
9857
+ this.showWarning("Cannot launch Swarm from /singular: no active model. Select one via /model and retry.");
9858
+ }
9859
+ else {
9860
+ this.showWarning("Cannot run /swarm: no active model selected. Configure /model first.");
9861
+ }
9862
+ return false;
9863
+ }
9864
+ resolveSwarmMaxParallel(input) {
9865
+ const requested = input.requested;
9866
+ if (typeof requested === "number" && Number.isFinite(requested)) {
9867
+ return Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, Math.floor(requested)));
9868
+ }
9869
+ const totalTasks = Math.max(1, input.plan.tasks.length);
9870
+ const initialFanout = Math.max(1, input.plan.tasks.filter((task) => task.depends_on.length === 0).length);
9871
+ const parallelizable = input.plan.tasks.filter((task) => task.concurrency_class === "implementation" || task.concurrency_class === "tests")
9872
+ .length;
9873
+ const sourceFloor = input.source === "singular" ? 4 : 3;
9874
+ const autoCap = Math.min(MAX_ORCHESTRATION_PARALLEL, 10);
9875
+ const heuristic = Math.max(sourceFloor, Math.ceil(totalTasks / 2), initialFanout, parallelizable >= 4 ? Math.ceil(parallelizable / 2) : 1);
9876
+ return Math.max(1, Math.min(autoCap, heuristic));
9877
+ }
9878
+ resolveSwarmDispatchTimeoutMs() {
9879
+ const dispatchTimeoutRaw = Number.parseInt(process.env.IOSM_SWARM_DISPATCH_TIMEOUT_MS ?? "", 10);
9880
+ if (Number.isInteger(dispatchTimeoutRaw) && dispatchTimeoutRaw > 0) {
9881
+ return Math.max(1_000, Math.min(1_800_000, dispatchTimeoutRaw));
9882
+ }
9883
+ return 180_000;
9884
+ }
9490
9885
  parseSwarmSpawnCandidates(output, parentTaskId) {
9491
9886
  const lines = output.split(/\r?\n/);
9492
9887
  const results = [];
@@ -9530,6 +9925,16 @@ export class InteractiveMode {
9530
9925
  }
9531
9926
  const profile = this.resolveSwarmTaskProfile(input.task);
9532
9927
  const delegateParallelHint = this.deriveSwarmTaskDelegateParallelHint(input.task);
9928
+ const requiresStrongDelegation = input.task.concurrency_class !== "analysis" &&
9929
+ input.task.concurrency_class !== "docs" &&
9930
+ (input.task.severity === "high" || delegateParallelHint >= 7);
9931
+ const minDelegatesRequired = !requiresStrongDelegation
9932
+ ? 0
9933
+ : delegateParallelHint >= 8
9934
+ ? 3
9935
+ : delegateParallelHint >= 5
9936
+ ? 2
9937
+ : 1;
9533
9938
  const safeDescription = input.task.brief.replace(/\s+/g, " ").trim().slice(0, 120).replace(/"/g, "'");
9534
9939
  const prompt = [
9535
9940
  `<swarm_task run_id="${input.meta.runId}" task_id="${input.task.id}" profile_hint="${profile}">`,
@@ -9542,10 +9947,15 @@ export class InteractiveMode {
9542
9947
  "",
9543
9948
  "Execution requirements:",
9544
9949
  `- 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>).",
9950
+ minDelegatesRequired > 0
9951
+ ? `- Inside this single task call, emit at least ${minDelegatesRequired} independent <delegate_task> subtasks (target parallel fan-out up to ${delegateParallelHint}).`
9952
+ : "- Delegation is optional for this task; keep execution focused.",
9953
+ minDelegatesRequired > 0
9954
+ ? '- If safe decomposition is impossible, output exactly one line: DELEGATION_IMPOSSIBLE: <reason>.'
9955
+ : "- If decomposition is not beneficial, continue with single-agent execution.",
9547
9956
  "- Keep edits inside declared scopes/touches. If scope expansion is required, explain and stop.",
9548
9957
  "- If blocked, respond with line: BLOCKED: <reason>",
9958
+ "- Return concise execution output; avoid long narrative if not needed.",
9549
9959
  "- Optional spawn candidates format: '- <description> | <path> | <change_type> | <low|medium|high>'",
9550
9960
  "</swarm_task>",
9551
9961
  ].join("\n");
@@ -9583,7 +9993,7 @@ export class InteractiveMode {
9583
9993
  resourceLoader,
9584
9994
  model,
9585
9995
  thinkingLevel: this.session.thinkingLevel,
9586
- profile: "full",
9996
+ profile: "meta",
9587
9997
  enableTaskTool: true,
9588
9998
  });
9589
9999
  swarmSession = created.session;
@@ -9600,6 +10010,14 @@ export class InteractiveMode {
9600
10010
  const taskErrors = [];
9601
10011
  let delegatedFailed = 0;
9602
10012
  let delegatedTasks = 0;
10013
+ let toolCallsStarted = 0;
10014
+ let toolCallsCompleted = 0;
10015
+ let assistantMessages = 0;
10016
+ let activeTool;
10017
+ const dispatchTimeoutMs = this.resolveSwarmDispatchTimeoutMs();
10018
+ let timedOut = false;
10019
+ let timeoutHandle;
10020
+ let detachStopListener;
9603
10021
  const delegatedFailureCauses = new Map();
9604
10022
  const accumulateFailureCauses = (raw) => {
9605
10023
  if (!raw || typeof raw !== "object")
@@ -9613,13 +10031,71 @@ export class InteractiveMode {
9613
10031
  delegatedFailureCauses.set(cause, (delegatedFailureCauses.get(cause) ?? 0) + numeric);
9614
10032
  }
9615
10033
  };
10034
+ const emitProgress = (progress) => {
10035
+ input.onProgress?.({
10036
+ activeTool,
10037
+ toolCallsStarted,
10038
+ toolCallsCompleted,
10039
+ assistantMessages,
10040
+ delegatedTasks,
10041
+ delegatedFailed,
10042
+ ...progress,
10043
+ });
10044
+ };
9616
10045
  const chunks = [];
9617
10046
  const unsubscribe = swarmSession.subscribe((event) => {
9618
- if (event.type === "tool_execution_start" && event.toolName === "task") {
9619
- taskToolCalls += 1;
10047
+ if (event.type === "tool_execution_start") {
10048
+ toolCallsStarted += 1;
10049
+ activeTool = event.toolName;
10050
+ if (event.toolName === "task") {
10051
+ taskToolCalls += 1;
10052
+ }
10053
+ emitProgress({
10054
+ phase: event.toolName ? `running ${event.toolName}` : "running",
10055
+ phaseState: "running",
10056
+ });
10057
+ return;
10058
+ }
10059
+ if (event.type === "tool_execution_update" && event.toolName === "task") {
10060
+ const partial = event.partialResult;
10061
+ const progressCandidate = partial?.details?.progress;
10062
+ if (progressCandidate && typeof progressCandidate === "object") {
10063
+ const progress = progressCandidate;
10064
+ const delegateItems = parseSubagentDelegateItems(progress.delegateItems);
10065
+ emitProgress({
10066
+ phase: typeof progress.message === "string" ? progress.message : undefined,
10067
+ phaseState: isSubagentPhaseState(progress.phase) ? progress.phase : undefined,
10068
+ cwd: typeof progress.cwd === "string" ? progress.cwd : undefined,
10069
+ activeTool: typeof progress.activeTool === "string" && progress.activeTool.trim().length > 0
10070
+ ? progress.activeTool.trim()
10071
+ : activeTool,
10072
+ toolCallsStarted: typeof progress.toolCallsStarted === "number" && Number.isFinite(progress.toolCallsStarted)
10073
+ ? progress.toolCallsStarted
10074
+ : undefined,
10075
+ toolCallsCompleted: typeof progress.toolCallsCompleted === "number" && Number.isFinite(progress.toolCallsCompleted)
10076
+ ? progress.toolCallsCompleted
10077
+ : undefined,
10078
+ assistantMessages: typeof progress.assistantMessages === "number" && Number.isFinite(progress.assistantMessages)
10079
+ ? progress.assistantMessages
10080
+ : undefined,
10081
+ delegateIndex: typeof progress.delegateIndex === "number" && Number.isFinite(progress.delegateIndex)
10082
+ ? progress.delegateIndex
10083
+ : undefined,
10084
+ delegateTotal: typeof progress.delegateTotal === "number" && Number.isFinite(progress.delegateTotal)
10085
+ ? progress.delegateTotal
10086
+ : undefined,
10087
+ delegateDescription: typeof progress.delegateDescription === "string" ? progress.delegateDescription : undefined,
10088
+ delegateProfile: typeof progress.delegateProfile === "string" ? progress.delegateProfile : undefined,
10089
+ delegateItems,
10090
+ });
10091
+ }
9620
10092
  return;
9621
10093
  }
9622
10094
  if (event.type === "tool_execution_end" && event.toolName === "task") {
10095
+ toolCallsCompleted += 1;
10096
+ if (activeTool === event.toolName) {
10097
+ activeTool = undefined;
10098
+ }
9623
10099
  const result = event.result;
9624
10100
  const details = result?.details;
9625
10101
  if (typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)) {
@@ -9629,38 +10105,127 @@ export class InteractiveMode {
9629
10105
  delegatedTasks += Math.max(0, details.delegatedTasks);
9630
10106
  }
9631
10107
  accumulateFailureCauses(details?.failureCauses);
10108
+ emitProgress({
10109
+ phase: event.isError ? "task tool failed" : "task tool completed",
10110
+ phaseState: "running",
10111
+ delegatedTasks: typeof details?.delegatedTasks === "number" && Number.isFinite(details.delegatedTasks)
10112
+ ? details.delegatedTasks
10113
+ : undefined,
10114
+ delegatedSucceeded: typeof details?.delegatedSucceeded === "number" && Number.isFinite(details.delegatedSucceeded)
10115
+ ? details.delegatedSucceeded
10116
+ : undefined,
10117
+ delegatedFailed: typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)
10118
+ ? details.delegatedFailed
10119
+ : undefined,
10120
+ });
9632
10121
  if (event.isError) {
9633
10122
  taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
9634
10123
  }
9635
10124
  return;
9636
10125
  }
10126
+ if (event.type === "tool_execution_end") {
10127
+ toolCallsCompleted += 1;
10128
+ if (activeTool === event.toolName) {
10129
+ activeTool = undefined;
10130
+ }
10131
+ emitProgress({
10132
+ phase: event.toolName ? `completed ${event.toolName}` : "running",
10133
+ phaseState: "running",
10134
+ });
10135
+ return;
10136
+ }
9637
10137
  if (event.type === "message_end" && event.message.role === "assistant") {
10138
+ assistantMessages += 1;
9638
10139
  for (const part of event.message.content) {
9639
10140
  if (part.type === "text" && part.text.trim()) {
9640
10141
  chunks.push(part.text.trim());
9641
10142
  }
9642
10143
  }
10144
+ emitProgress({
10145
+ phase: "drafting response",
10146
+ phaseState: "responding",
10147
+ });
9643
10148
  }
9644
10149
  });
9645
10150
  try {
9646
- await swarmSession.prompt(prompt, {
10151
+ emitProgress({
10152
+ phase: "booting subagent",
10153
+ phaseState: "starting",
10154
+ });
10155
+ if (input.stopSignal?.aborted) {
10156
+ return {
10157
+ taskId: input.task.id,
10158
+ status: "blocked",
10159
+ error: "Swarm run interrupted.",
10160
+ failureCause: "interrupted",
10161
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
10162
+ };
10163
+ }
10164
+ const promptPromise = swarmSession.prompt(prompt, {
9647
10165
  expandPromptTemplates: false,
9648
10166
  skipIosmAutopilot: true,
9649
10167
  skipOrchestrationDirective: true,
9650
10168
  source: "interactive",
9651
10169
  });
10170
+ // Guard against provider/model hangs in isolated swarm sessions.
10171
+ void promptPromise.catch(() => {
10172
+ // handled by race below
10173
+ });
10174
+ const timeoutPromise = new Promise((_, reject) => {
10175
+ timeoutHandle = setTimeout(() => {
10176
+ timedOut = true;
10177
+ void swarmSession.abort().catch(() => {
10178
+ // best effort
10179
+ });
10180
+ emitProgress({
10181
+ phase: "dispatch timeout",
10182
+ phaseState: "responding",
10183
+ });
10184
+ reject(new Error(`Swarm task dispatch timed out after ${dispatchTimeoutMs}ms.`));
10185
+ }, dispatchTimeoutMs);
10186
+ });
10187
+ const stopPromise = new Promise((_, reject) => {
10188
+ if (!input.stopSignal)
10189
+ return;
10190
+ const onAbort = () => {
10191
+ void swarmSession.abort().catch(() => {
10192
+ // best effort
10193
+ });
10194
+ emitProgress({
10195
+ phase: "dispatch interrupted",
10196
+ phaseState: "responding",
10197
+ });
10198
+ reject(new Error("Swarm task dispatch interrupted."));
10199
+ };
10200
+ if (input.stopSignal.aborted) {
10201
+ onAbort();
10202
+ return;
10203
+ }
10204
+ input.stopSignal.addEventListener("abort", onAbort, { once: true });
10205
+ detachStopListener = () => input.stopSignal?.removeEventListener("abort", onAbort);
10206
+ });
10207
+ await Promise.race([promptPromise, timeoutPromise, stopPromise]);
9652
10208
  }
9653
10209
  catch (error) {
9654
10210
  return {
9655
10211
  taskId: input.task.id,
9656
10212
  status: "error",
9657
10213
  error: error instanceof Error ? error.message : String(error),
10214
+ failureCause: error instanceof Error && /interrupted/i.test(error.message)
10215
+ ? "interrupted"
10216
+ : timedOut
10217
+ ? "timeout"
10218
+ : undefined,
9658
10219
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9659
10220
  };
9660
10221
  }
9661
10222
  finally {
10223
+ detachStopListener?.();
10224
+ if (timeoutHandle) {
10225
+ clearTimeout(timeoutHandle);
10226
+ }
9662
10227
  unsubscribe();
9663
- if (swarmSession.isStreaming) {
10228
+ if (timedOut || swarmSession.isStreaming) {
9664
10229
  await swarmSession.abort().catch(() => {
9665
10230
  // best effort
9666
10231
  });
@@ -9682,6 +10247,7 @@ export class InteractiveMode {
9682
10247
  taskId: input.task.id,
9683
10248
  status: "error",
9684
10249
  error: "No task tool call executed by assistant.",
10250
+ failureCause: "protocol_violation",
9685
10251
  costUsd: this.estimateSwarmTaskCostUsd(input.task),
9686
10252
  };
9687
10253
  }
@@ -9723,6 +10289,7 @@ export class InteractiveMode {
9723
10289
  if (!input.resumeState) {
9724
10290
  store.init(input.meta, input.plan, initialState);
9725
10291
  }
10292
+ const swarmTaskById = new Map(input.plan.tasks.map((task) => [task.id, task]));
9726
10293
  let rollingIndex = input.projectIndex;
9727
10294
  let localStopRequested = false;
9728
10295
  const refreshIncrementalIndex = () => {
@@ -9740,57 +10307,243 @@ export class InteractiveMode {
9740
10307
  rollingIndex = rebuilt;
9741
10308
  };
9742
10309
  this.swarmActiveRunId = input.runId;
10310
+ this.swarmStopRequested = false;
10311
+ this.swarmAbortController = new AbortController();
10312
+ this.footerDataProvider.setSwarmBusy(true);
10313
+ this.footer.invalidate();
9743
10314
  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,
10315
+ let schedulerResult;
10316
+ try {
10317
+ schedulerResult = await runSwarmScheduler({
10318
+ runId: input.runId,
10319
+ plan: input.plan,
9755
10320
  contract: input.contract,
9756
- }),
9757
- confirmSpawn: async ({ candidate, parentTask }) => {
9758
- const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
9759
- if (!requiresConfirmation)
10321
+ maxParallel: input.meta.maxParallel,
10322
+ budgetUsd: input.budgetUsd,
10323
+ existingState: initialState,
10324
+ dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
10325
+ meta: input.meta,
10326
+ task,
10327
+ runtime,
10328
+ contract: input.contract,
10329
+ stopSignal: this.swarmAbortController?.signal,
10330
+ onProgress: (progress) => {
10331
+ this.updateSwarmSubagentProgress({
10332
+ runId: input.runId,
10333
+ taskId: task.id,
10334
+ task,
10335
+ profile: this.resolveSwarmTaskProfile(task),
10336
+ progress,
10337
+ });
10338
+ },
10339
+ }),
10340
+ confirmSpawn: async ({ candidate, parentTask }) => {
10341
+ const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
10342
+ if (!requiresConfirmation)
10343
+ return true;
10344
+ const choice = await this.showExtensionSelector([
10345
+ `/swarm spawn candidate requires confirmation`,
10346
+ `severity=${candidate.severity} task=${parentTask.id}`,
10347
+ `description=${candidate.description}`,
10348
+ `path=${candidate.path}`,
10349
+ ].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
10350
+ if (!choice || choice.startsWith("Reject")) {
10351
+ this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
10352
+ return false;
10353
+ }
10354
+ if (choice === "Abort run") {
10355
+ localStopRequested = true;
10356
+ this.showWarning("Swarm run marked to stop after current scheduling step.");
10357
+ return false;
10358
+ }
9760
10359
  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;
10360
+ },
10361
+ dispatchTimeoutMs: Math.min(1_800_000, this.resolveSwarmDispatchTimeoutMs() + 5_000),
10362
+ onEvent: (event) => {
10363
+ store.appendEvent(event);
10364
+ if (event.type === "task_running" && event.taskId) {
10365
+ const task = swarmTaskById.get(event.taskId);
10366
+ if (task) {
10367
+ this.updateSwarmSubagentProgress({
10368
+ runId: input.runId,
10369
+ taskId: task.id,
10370
+ task,
10371
+ profile: this.resolveSwarmTaskProfile(task),
10372
+ progress: {
10373
+ phase: "starting subagent",
10374
+ phaseState: "starting",
10375
+ },
10376
+ });
10377
+ }
10378
+ this.showStatus(`Swarm ${event.taskId}: running`);
10379
+ }
10380
+ if (event.type === "task_done" && event.taskId) {
10381
+ this.finalizeSwarmSubagentDisplay({
10382
+ runId: input.runId,
10383
+ taskId: event.taskId,
10384
+ status: "done",
10385
+ });
10386
+ this.showStatus(`Swarm ${event.taskId}: done`);
10387
+ }
10388
+ if (event.type === "task_retry" && event.taskId) {
10389
+ const task = swarmTaskById.get(event.taskId);
10390
+ if (task) {
10391
+ this.updateSwarmSubagentProgress({
10392
+ runId: input.runId,
10393
+ taskId: event.taskId,
10394
+ task,
10395
+ profile: this.resolveSwarmTaskProfile(task),
10396
+ progress: {
10397
+ phase: event.message,
10398
+ phaseState: "running",
10399
+ },
10400
+ });
10401
+ }
10402
+ this.showWarning(`Swarm ${event.taskId}: ${event.message}`);
10403
+ }
10404
+ if (event.type === "task_error" && event.taskId) {
10405
+ this.finalizeSwarmSubagentDisplay({
10406
+ runId: input.runId,
10407
+ taskId: event.taskId,
10408
+ status: "error",
10409
+ errorMessage: event.message,
10410
+ });
10411
+ this.showWarning(`Swarm ${event.taskId} failed: ${event.message}`);
10412
+ }
10413
+ if (event.type === "task_blocked" && event.taskId) {
10414
+ this.finalizeSwarmSubagentDisplay({
10415
+ runId: input.runId,
10416
+ taskId: event.taskId,
10417
+ status: "error",
10418
+ errorMessage: event.message,
10419
+ });
10420
+ this.showWarning(`Swarm ${event.taskId} blocked: ${event.message}`);
10421
+ }
10422
+ if ((event.type === "run_blocked" || event.type === "run_failed" || event.type === "run_stopped") && event.message) {
10423
+ this.showWarning(`Swarm ${event.type.replace("run_", "")}: ${event.message}`);
10424
+ }
10425
+ },
10426
+ onStateChanged: (state) => {
10427
+ store.saveState(state);
10428
+ store.saveCheckpoint(state);
10429
+ refreshIncrementalIndex();
10430
+ },
10431
+ shouldStop: () => this.shutdownRequested || localStopRequested || this.swarmStopRequested,
10432
+ });
10433
+ }
10434
+ catch (error) {
10435
+ const message = error instanceof Error ? error.message : String(error);
10436
+ const failedState = store.loadState() ?? initialState;
10437
+ failedState.status = "failed";
10438
+ failedState.lastError = message;
10439
+ failedState.updatedAt = new Date().toISOString();
10440
+ store.appendEvent({
10441
+ type: "run_failed",
10442
+ timestamp: new Date().toISOString(),
10443
+ runId: input.runId,
10444
+ tick: failedState.tick,
10445
+ message,
10446
+ });
10447
+ store.saveState(failedState);
10448
+ store.saveCheckpoint(failedState);
10449
+ this.finalizeSwarmRunSubagentDisplays(input.runId, message);
10450
+ this.showWarning(`Swarm run failed unexpectedly: ${message}`);
10451
+ return;
10452
+ }
10453
+ finally {
10454
+ this.swarmActiveRunId = undefined;
10455
+ this.swarmStopRequested = false;
10456
+ this.swarmAbortController = undefined;
10457
+ this.footerDataProvider.setSwarmBusy(false);
10458
+ this.footer.invalidate();
10459
+ }
10460
+ if (schedulerResult.state.status !== "completed") {
10461
+ this.finalizeSwarmRunSubagentDisplays(input.runId, schedulerResult.state.lastError ?? `Swarm run ${schedulerResult.state.status}`);
10462
+ }
9789
10463
  const taskStates = Object.values(schedulerResult.state.tasks);
9790
10464
  const doneCount = taskStates.filter((task) => task.status === "done").length;
9791
10465
  const errorCount = taskStates.filter((task) => task.status === "error").length;
9792
10466
  const blockedCount = taskStates.filter((task) => task.status === "blocked").length;
9793
10467
  const total = taskStates.length;
10468
+ const summaryByTask = new Map();
10469
+ let summaryKeysMatched = 0;
10470
+ let findingKeysMatched = 0;
10471
+ try {
10472
+ const summaryRead = await readSharedMemory({
10473
+ rootCwd: cwd,
10474
+ runId: input.runId,
10475
+ }, {
10476
+ scope: "run",
10477
+ prefix: "results/",
10478
+ includeValues: true,
10479
+ limit: Math.max(50, total * 6),
10480
+ });
10481
+ summaryKeysMatched = summaryRead.totalMatched;
10482
+ for (const item of summaryRead.items) {
10483
+ const key = item.key.trim();
10484
+ if (!key.startsWith("results/"))
10485
+ continue;
10486
+ const taskId = key.slice("results/".length).trim();
10487
+ if (!taskId)
10488
+ continue;
10489
+ let parsedSummary;
10490
+ if (item.value) {
10491
+ try {
10492
+ const parsed = JSON.parse(item.value);
10493
+ parsedSummary = {
10494
+ delegatedTotal: parsed.delegated?.total,
10495
+ delegatedSucceeded: parsed.delegated?.succeeded,
10496
+ delegatedFailed: parsed.delegated?.failed,
10497
+ summary: typeof parsed.summary === "string" ? parsed.summary : undefined,
10498
+ };
10499
+ }
10500
+ catch {
10501
+ // keep best-effort behavior for malformed summary payloads
10502
+ }
10503
+ }
10504
+ summaryByTask.set(taskId, parsedSummary ?? {});
10505
+ }
10506
+ const findingsRead = await readSharedMemory({
10507
+ rootCwd: cwd,
10508
+ runId: input.runId,
10509
+ }, {
10510
+ scope: "run",
10511
+ prefix: "findings/",
10512
+ includeValues: false,
10513
+ limit: Math.max(100, total * 12),
10514
+ });
10515
+ findingKeysMatched = findingsRead.totalMatched;
10516
+ }
10517
+ catch {
10518
+ // shared-memory aggregation is advisory, never fail swarm completion on it
10519
+ }
10520
+ const missingSummaryTasks = input.plan.tasks
10521
+ .map((task) => task.id)
10522
+ .filter((taskId) => !summaryByTask.has(taskId));
10523
+ const sharedMemoryLines = [
10524
+ "## Shared Memory Coordination",
10525
+ `- result_keys: ${summaryKeysMatched}`,
10526
+ `- finding_keys: ${findingKeysMatched}`,
10527
+ `- task_summaries_found: ${summaryByTask.size}/${total}`,
10528
+ missingSummaryTasks.length > 0
10529
+ ? `- missing_task_summaries: ${missingSummaryTasks.slice(0, 12).join(", ")}`
10530
+ : "- missing_task_summaries: none",
10531
+ ...input.plan.tasks.slice(0, 12).map((task) => {
10532
+ const summary = summaryByTask.get(task.id);
10533
+ if (!summary)
10534
+ return `- ${task.id}: no summary key`;
10535
+ const delegatedPart = typeof summary.delegatedTotal === "number" &&
10536
+ typeof summary.delegatedSucceeded === "number" &&
10537
+ typeof summary.delegatedFailed === "number"
10538
+ ? `delegated=${summary.delegatedSucceeded}/${summary.delegatedTotal} (failed=${summary.delegatedFailed})`
10539
+ : "delegated=n/a";
10540
+ const summaryExcerpt = typeof summary.summary === "string" && summary.summary.trim().length > 0
10541
+ ? summary.summary.trim().replace(/\s+/g, " ").slice(0, 120)
10542
+ : "no summary excerpt";
10543
+ return `- ${task.id}: ${delegatedPart}; ${summaryExcerpt}`;
10544
+ }),
10545
+ "",
10546
+ ];
9794
10547
  const reportLines = [
9795
10548
  "# Swarm Integration Report",
9796
10549
  "",
@@ -9812,6 +10565,7 @@ export class InteractiveMode {
9812
10565
  ...schedulerResult.runGate.failures.map((item) => `- fail: ${item}`),
9813
10566
  ...schedulerResult.runGate.warnings.map((item) => `- warn: ${item}`),
9814
10567
  "",
10568
+ ...sharedMemoryLines,
9815
10569
  "## Spawn Backlog",
9816
10570
  ...(schedulerResult.spawnBacklog.length > 0
9817
10571
  ? schedulerResult.spawnBacklog.map((item) => `- ${item.description} | ${item.path} | ${item.changeType} | fp=${item.fingerprint}`)
@@ -9828,6 +10582,7 @@ export class InteractiveMode {
9828
10582
  "# Shared Context",
9829
10583
  "",
9830
10584
  `Run ${input.runId} finished with status: ${schedulerResult.state.status}.`,
10585
+ `Shared memory summaries: ${summaryByTask.size}/${total} task result key(s), findings=${findingKeysMatched}.`,
9831
10586
  `Recommendation: ${schedulerResult.runGate.pass ? "proceed to /iosm for measurable optimization" : "resolve failed gates before /iosm"}.`,
9832
10587
  ].join("\n"),
9833
10588
  });
@@ -9836,11 +10591,14 @@ export class InteractiveMode {
9836
10591
  `status: ${schedulerResult.state.status}`,
9837
10592
  `tasks: ${doneCount}/${total} done · ${errorCount} error · ${blockedCount} blocked`,
9838
10593
  `budget_usd: ${schedulerResult.state.budget.spentUsd.toFixed(2)}${input.meta.budgetUsd ? `/${input.meta.budgetUsd.toFixed(2)}` : ""}`,
10594
+ `shared_memory: summaries ${summaryByTask.size}/${total} · findings ${findingKeysMatched}`,
9839
10595
  `watch: /swarm watch ${input.runId}`,
9840
10596
  `resume: /swarm resume ${input.runId}`,
9841
10597
  ].join("\n"));
9842
10598
  }
9843
10599
  async runSwarmFromTask(task, options) {
10600
+ if (!this.ensureSwarmModelReady("plain"))
10601
+ return;
9844
10602
  const contract = await this.ensureSwarmEffectiveContract(task);
9845
10603
  if (!contract)
9846
10604
  return;
@@ -9855,7 +10613,11 @@ export class InteractiveMode {
9855
10613
  index: indexInfo.index,
9856
10614
  });
9857
10615
  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));
10616
+ const maxParallel = this.resolveSwarmMaxParallel({
10617
+ requested: options.maxParallel,
10618
+ plan,
10619
+ source: "plain",
10620
+ });
9859
10621
  const meta = this.buildSwarmRunMeta({
9860
10622
  runId,
9861
10623
  source: "plain",
@@ -9877,6 +10639,8 @@ export class InteractiveMode {
9877
10639
  });
9878
10640
  }
9879
10641
  async runSwarmFromSingular(input) {
10642
+ if (!this.ensureSwarmModelReady("singular"))
10643
+ return;
9880
10644
  const analysis = this.loadSingularAnalysisByRunId(input.runId);
9881
10645
  if (!analysis) {
9882
10646
  this.showWarning(`Singular run not found: ${input.runId}`);
@@ -9899,7 +10663,11 @@ export class InteractiveMode {
9899
10663
  index: indexInfo.index,
9900
10664
  });
9901
10665
  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));
10666
+ const maxParallel = this.resolveSwarmMaxParallel({
10667
+ requested: input.maxParallel,
10668
+ plan,
10669
+ source: "singular",
10670
+ });
9903
10671
  const meta = this.buildSwarmRunMeta({
9904
10672
  runId,
9905
10673
  source: "singular",
@@ -10087,6 +10855,10 @@ export class InteractiveMode {
10087
10855
  });
10088
10856
  }
10089
10857
  async handleSwarmCommand(text) {
10858
+ if (this.swarmActiveRunId) {
10859
+ this.showWarning(`Swarm run already in progress: ${this.swarmActiveRunId}. Use /swarm watch.`);
10860
+ return;
10861
+ }
10090
10862
  if (this.session.isStreaming) {
10091
10863
  this.showWarning("Cannot run /swarm while the agent is processing another request.");
10092
10864
  return;
@@ -10300,6 +11072,9 @@ export class InteractiveMode {
10300
11072
  this.showWarning("--max-parallel is only valid with --parallel mode.");
10301
11073
  return undefined;
10302
11074
  }
11075
+ if (mode === "parallel" && maxParallel === undefined) {
11076
+ maxParallel = Math.max(1, Math.min(MAX_ORCHESTRATION_PARALLEL, agents));
11077
+ }
10303
11078
  if (maxParallel !== undefined && maxParallel > agents) {
10304
11079
  maxParallel = agents;
10305
11080
  }
@@ -10366,11 +11141,24 @@ export class InteractiveMode {
10366
11141
  const currentCwd = this.sessionManager.getCwd();
10367
11142
  const assignments = [];
10368
11143
  const assignmentRecords = [];
11144
+ const dependencyEdges = parsed.dependencies?.reduce((sum, entry) => sum + entry.dependsOn.length, 0) ?? 0;
11145
+ const defaultAssignmentProfile = this.resolveOrchestrateDefaultAssignmentProfile(parsed);
11146
+ const delegateParallelHints = [];
10369
11147
  for (let index = 0; index < parsed.agents; index++) {
10370
- const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? (this.activeProfileName || "full");
11148
+ const assignmentProfile = parsed.profiles?.[index] ?? parsed.profile ?? defaultAssignmentProfile;
10371
11149
  const assignmentCwd = parsed.cwds?.[index] ?? ".";
10372
11150
  const assignmentLock = parsed.locks?.[index];
10373
11151
  const dependsOn = parsed.dependencies?.find((entry) => entry.agent === index + 1)?.dependsOn ?? [];
11152
+ const delegateParallelHint = this.deriveOrchestrateDelegateParallelHint({
11153
+ task: parsed.task,
11154
+ mode: parsed.mode,
11155
+ agents: parsed.agents,
11156
+ maxParallel: parsed.maxParallel,
11157
+ dependencyEdges,
11158
+ hasLock: !!assignmentLock,
11159
+ hasDependencies: dependsOn.length > 0,
11160
+ });
11161
+ delegateParallelHints.push(delegateParallelHint);
10374
11162
  const resolvedCwd = path.resolve(currentCwd, assignmentCwd);
10375
11163
  assignmentRecords.push({
10376
11164
  profile: assignmentProfile,
@@ -10378,7 +11166,8 @@ export class InteractiveMode {
10378
11166
  lockKey: assignmentLock,
10379
11167
  dependsOn,
10380
11168
  });
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("|")}` : ""}`);
11169
+ 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}
11170
+ }`);
10382
11171
  }
10383
11172
  const teamRun = createTeamRun({
10384
11173
  cwd: currentCwd,
@@ -10391,8 +11180,16 @@ export class InteractiveMode {
10391
11180
  const runAssignments = teamRun.tasks.map((task, index) => `${assignments[index]} run_id=${teamRun.runId} task_id=${task.id}`);
10392
11181
  const taskCallHints = teamRun.tasks.map((task, index) => {
10393
11182
  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"' : ""}`;
11183
+ const hint = delegateParallelHints[index] ?? 1;
11184
+ 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
11185
  });
11186
+ if (parsed.mode === "parallel" &&
11187
+ !parsed.profile &&
11188
+ !(parsed.profiles && parsed.profiles.length > 0) &&
11189
+ defaultAssignmentProfile === "meta" &&
11190
+ this.activeProfileName !== "meta") {
11191
+ this.showStatus("Orchestrate auto-profile: using `meta` workers for stronger fan-out and nested delegation.");
11192
+ }
10396
11193
  this.chatContainer.addChild(new Spacer(1));
10397
11194
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "◆ ")) + theme.bold("/orchestrate") + theme.fg("muted", ` ${currentCwd}`), 1, 0));
10398
11195
  this.ui.requestRender();
@@ -10407,9 +11204,15 @@ export class InteractiveMode {
10407
11204
  "- for parallel mode, emit all independent task calls in one assistant response",
10408
11205
  "- in parallel mode, use parallel tool-call style (<use_parallel_tool_calls>)",
10409
11206
  "- when assignment lines include depends_on, still emit one task call per assignment; runtime enforces dependency gating",
11207
+ "- include delegate_parallel_hint from each assignment/task_call hint in every corresponding task tool call",
11208
+ "- for delegate_parallel_hint >= 2, split child work into nested <delegate_task> streams unless impossible",
11209
+ "- if nested split is impossible for a non-trivial stream, emit one line: DELEGATION_IMPOSSIBLE: <reason>",
10410
11210
  "- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
10411
11211
  "- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
10412
11212
  "- include run_id and task_id from each assignment in the task tool arguments",
11213
+ "- publish one run-scoped shared-memory summary key per assignment (results/<task_id>) before final synthesis",
11214
+ "- in final synthesis, only report metrics backed by observed run evidence (task details, shared-memory keys, test output, or verified files); otherwise mark them unknown",
11215
+ "- never claim report/artifact files exist unless created in this run or verified on disk",
10413
11216
  "- keep each agent in its assigned cwd",
10414
11217
  "- avoid edit collisions; if two write-capable agents target same area, serialize those writes",
10415
11218
  "- aggregate outputs into one concise final synthesis",
@@ -12394,6 +13197,10 @@ The agent will automatically receive IOSM context on every turn.`;
12394
13197
  }
12395
13198
  }
12396
13199
  async handleBashCommand(command, excludeFromContext = false) {
13200
+ if (isReadOnlyProfileName(this.activeProfileName)) {
13201
+ this.showWarning(`Bash is disabled in ${this.activeProfileName} profile. Switch to full/meta/iosm (Shift+Tab).`);
13202
+ return;
13203
+ }
12397
13204
  const extensionRunner = this.session.extensionRunner;
12398
13205
  // Emit user_bash event to let extensions intercept
12399
13206
  const eventResult = extensionRunner