iosm-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +64 -52
  3. package/dist/core/agent-teams.d.ts.map +1 -1
  4. package/dist/core/agent-teams.js +38 -11
  5. package/dist/core/agent-teams.js.map +1 -1
  6. package/dist/core/failure-retrospective.d.ts +12 -0
  7. package/dist/core/failure-retrospective.d.ts.map +1 -0
  8. package/dist/core/failure-retrospective.js +115 -0
  9. package/dist/core/failure-retrospective.js.map +1 -0
  10. package/dist/core/model-registry.d.ts.map +1 -1
  11. package/dist/core/model-registry.js +2 -3
  12. package/dist/core/model-registry.js.map +1 -1
  13. package/dist/core/models-dev-provider-catalog.d.ts +30 -0
  14. package/dist/core/models-dev-provider-catalog.d.ts.map +1 -0
  15. package/dist/core/models-dev-provider-catalog.js +118 -0
  16. package/dist/core/models-dev-provider-catalog.js.map +1 -0
  17. package/dist/core/models-dev-providers.d.ts +12 -0
  18. package/dist/core/models-dev-providers.d.ts.map +1 -0
  19. package/dist/core/models-dev-providers.js +736 -0
  20. package/dist/core/models-dev-providers.js.map +1 -0
  21. package/dist/core/project-index/index.d.ts +17 -0
  22. package/dist/core/project-index/index.d.ts.map +1 -0
  23. package/dist/core/project-index/index.js +323 -0
  24. package/dist/core/project-index/index.js.map +1 -0
  25. package/dist/core/project-index/types.d.ts +34 -0
  26. package/dist/core/project-index/types.d.ts.map +1 -0
  27. package/dist/core/project-index/types.js +2 -0
  28. package/dist/core/project-index/types.js.map +1 -0
  29. package/dist/core/sdk.d.ts.map +1 -1
  30. package/dist/core/sdk.js +8 -0
  31. package/dist/core/sdk.js.map +1 -1
  32. package/dist/core/shared-memory.d.ts +46 -0
  33. package/dist/core/shared-memory.d.ts.map +1 -0
  34. package/dist/core/shared-memory.js +253 -0
  35. package/dist/core/shared-memory.js.map +1 -0
  36. package/dist/core/slash-commands.d.ts.map +1 -1
  37. package/dist/core/slash-commands.js +5 -1
  38. package/dist/core/slash-commands.js.map +1 -1
  39. package/dist/core/subagents.js +1 -1
  40. package/dist/core/subagents.js.map +1 -1
  41. package/dist/core/swarm/gates.d.ts +9 -0
  42. package/dist/core/swarm/gates.d.ts.map +1 -0
  43. package/dist/core/swarm/gates.js +65 -0
  44. package/dist/core/swarm/gates.js.map +1 -0
  45. package/dist/core/swarm/index.d.ts +9 -0
  46. package/dist/core/swarm/index.d.ts.map +1 -0
  47. package/dist/core/swarm/index.js +9 -0
  48. package/dist/core/swarm/index.js.map +1 -0
  49. package/dist/core/swarm/locks.d.ts +21 -0
  50. package/dist/core/swarm/locks.d.ts.map +1 -0
  51. package/dist/core/swarm/locks.js +93 -0
  52. package/dist/core/swarm/locks.js.map +1 -0
  53. package/dist/core/swarm/planner.d.ts +16 -0
  54. package/dist/core/swarm/planner.d.ts.map +1 -0
  55. package/dist/core/swarm/planner.js +137 -0
  56. package/dist/core/swarm/planner.js.map +1 -0
  57. package/dist/core/swarm/retry.d.ts +16 -0
  58. package/dist/core/swarm/retry.d.ts.map +1 -0
  59. package/dist/core/swarm/retry.js +32 -0
  60. package/dist/core/swarm/retry.js.map +1 -0
  61. package/dist/core/swarm/scheduler.d.ts +48 -0
  62. package/dist/core/swarm/scheduler.d.ts.map +1 -0
  63. package/dist/core/swarm/scheduler.js +554 -0
  64. package/dist/core/swarm/scheduler.js.map +1 -0
  65. package/dist/core/swarm/spawn.d.ts +16 -0
  66. package/dist/core/swarm/spawn.d.ts.map +1 -0
  67. package/dist/core/swarm/spawn.js +42 -0
  68. package/dist/core/swarm/spawn.js.map +1 -0
  69. package/dist/core/swarm/state-store.d.ts +35 -0
  70. package/dist/core/swarm/state-store.d.ts.map +1 -0
  71. package/dist/core/swarm/state-store.js +106 -0
  72. package/dist/core/swarm/state-store.js.map +1 -0
  73. package/dist/core/swarm/types.d.ts +116 -0
  74. package/dist/core/swarm/types.d.ts.map +1 -0
  75. package/dist/core/swarm/types.js +2 -0
  76. package/dist/core/swarm/types.js.map +1 -0
  77. package/dist/core/system-prompt.d.ts.map +1 -1
  78. package/dist/core/system-prompt.js +3 -2
  79. package/dist/core/system-prompt.js.map +1 -1
  80. package/dist/core/tools/shared-memory.d.ts +23 -0
  81. package/dist/core/tools/shared-memory.d.ts.map +1 -0
  82. package/dist/core/tools/shared-memory.js +134 -0
  83. package/dist/core/tools/shared-memory.js.map +1 -0
  84. package/dist/core/tools/task.d.ts +8 -1
  85. package/dist/core/tools/task.d.ts.map +1 -1
  86. package/dist/core/tools/task.js +664 -123
  87. package/dist/core/tools/task.js.map +1 -1
  88. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/footer.js +3 -11
  90. package/dist/modes/interactive/components/footer.js.map +1 -1
  91. package/dist/modes/interactive/components/login-dialog.d.ts +1 -0
  92. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  93. package/dist/modes/interactive/components/login-dialog.js +27 -4
  94. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  95. package/dist/modes/interactive/components/oauth-selector.d.ts +13 -1
  96. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  97. package/dist/modes/interactive/components/oauth-selector.js +89 -27
  98. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  99. package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
  100. package/dist/modes/interactive/components/subagent-message.js +14 -0
  101. package/dist/modes/interactive/components/subagent-message.js.map +1 -1
  102. package/dist/modes/interactive/interactive-mode.d.ts +50 -0
  103. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  104. package/dist/modes/interactive/interactive-mode.js +1594 -51
  105. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  106. package/docs/cli-reference.md +11 -1
  107. package/docs/configuration.md +4 -1
  108. package/docs/getting-started.md +2 -2
  109. package/docs/interactive-mode.md +43 -4
  110. package/docs/orchestration-and-subagents.md +96 -169
  111. package/package.json +5 -4
@@ -15,16 +15,20 @@ 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 { loadModelsDevProviderCatalog, } from "../../core/models-dev-provider-catalog.js";
18
19
  import { ModelRegistry } from "../../core/model-registry.js";
20
+ import { MODELS_DEV_PROVIDERS } from "../../core/models-dev-providers.js";
19
21
  import { resolveModelScope } from "../../core/model-resolver.js";
20
22
  import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
21
23
  import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
22
24
  import { ContractService, normalizeEngineeringContract, } from "../../core/contract.js";
25
+ import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadProjectIndex, queryProjectIndex, saveProjectIndex, } from "../../core/project-index/index.js";
23
26
  import { SingularService, } from "../../core/singular.js";
24
27
  import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
25
28
  import { DefaultResourceLoader } from "../../core/resource-loader.js";
26
29
  import { createAgentSession } from "../../core/sdk.js";
27
30
  import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
31
+ import { buildSwarmPlanFromSingular, buildSwarmPlanFromTask, runSwarmScheduler, SwarmStateStore, } from "../../core/swarm/index.js";
28
32
  import { loadCustomSubagents, resolveCustomSubagentReference, } from "../../core/subagents.js";
29
33
  import { getSubagentRun, listSubagentRuns } from "../../core/subagent-runs.js";
30
34
  import { SessionManager } from "../../core/session-manager.js";
@@ -270,6 +274,41 @@ function resolveDoctorCliToolStatuses() {
270
274
  });
271
275
  }
272
276
  const OPENROUTER_PROVIDER_ID = "openrouter";
277
+ const PROVIDER_DISPLAY_NAME_OVERRIDES = {
278
+ "azure-openai-responses": "Azure OpenAI Responses",
279
+ "google-antigravity": "Google Antigravity",
280
+ "google-gemini-cli": "Google Gemini CLI",
281
+ "kimi-coding": "Kimi Coding",
282
+ "openai-codex": "OpenAI Codex",
283
+ "opencode-go": "OpenCode Go",
284
+ "vercel-ai-gateway": "Vercel AI Gateway",
285
+ };
286
+ function toProviderDisplayName(providerId) {
287
+ const override = PROVIDER_DISPLAY_NAME_OVERRIDES[providerId];
288
+ if (override)
289
+ return override;
290
+ return providerId
291
+ .split(/[-_]/g)
292
+ .map((part) => {
293
+ const lower = part.toLowerCase();
294
+ if (lower === "ai")
295
+ return "AI";
296
+ if (lower === "api")
297
+ return "API";
298
+ if (lower === "gpt")
299
+ return "GPT";
300
+ if (lower === "aws")
301
+ return "AWS";
302
+ if (lower === "ui")
303
+ return "UI";
304
+ if (lower === "llm")
305
+ return "LLM";
306
+ if (lower === "id")
307
+ return "ID";
308
+ return part.charAt(0).toUpperCase() + part.slice(1);
309
+ })
310
+ .join(" ");
311
+ }
273
312
  function isAbortLikeMessage(message) {
274
313
  const normalized = message.trim().toLowerCase();
275
314
  return normalized.includes("aborted") || normalized.includes("cancelled");
@@ -362,6 +401,7 @@ export class InteractiveMode {
362
401
  this.permissionPromptLock = Promise.resolve();
363
402
  this.sessionAllowedToolSignatures = new Set();
364
403
  this.singularLastEffectiveContract = {};
404
+ this.swarmActiveRunId = undefined;
365
405
  this.lastSigintTime = 0;
366
406
  this.lastEscapeTime = 0;
367
407
  this.changelogMarkdown = undefined;
@@ -375,6 +415,7 @@ export class InteractiveMode {
375
415
  this.pendingTools = new Map();
376
416
  // Subagent execution tracking with live progress/metadata for task tool calls.
377
417
  this.subagentComponents = new Map();
418
+ this.subagentElapsedTimer = undefined;
378
419
  // Internal UI metadata emitted by runtime for orchestration rendering.
379
420
  this.pendingInternalUserDisplayAliases = [];
380
421
  this.pendingAssistantOrchestrationContexts = 0;
@@ -417,6 +458,17 @@ export class InteractiveMode {
417
458
  this.builtInHeader = undefined;
418
459
  // ASCII logo component for startup screen
419
460
  this.asciiLogo = undefined;
461
+ // API-key provider labels cached for /login and status messages.
462
+ this.apiKeyProviderDisplayNames = new Map();
463
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
464
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
465
+ provider.id,
466
+ {
467
+ ...provider,
468
+ models: [],
469
+ },
470
+ ]));
471
+ this.modelsDevProviderCatalogRefreshPromise = undefined;
420
472
  // Custom header from extension (undefined = use built-in header)
421
473
  this.customHeader = undefined;
422
474
  /**
@@ -1010,6 +1062,32 @@ export class InteractiveMode {
1010
1062
  }
1011
1063
  return null;
1012
1064
  }
1065
+ getSwarmArgumentCompletions(prefix) {
1066
+ const subcommands = ["run", "from-singular", "watch", "retry", "resume", "help"];
1067
+ const hasTrailingSpace = /\\s$/.test(prefix);
1068
+ const tokens = this.parseSlashArgs(prefix);
1069
+ const first = tokens[0]?.toLowerCase();
1070
+ if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
1071
+ const query = first ?? "";
1072
+ return subcommands.filter((item) => item.includes(query)).map((item) => ({ value: item, label: item }));
1073
+ }
1074
+ const active = first;
1075
+ if (active === "run" || active === "from-singular") {
1076
+ const flags = ["--max-parallel", "--budget-usd", ...(active === "from-singular" ? ["--option"] : [])];
1077
+ const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
1078
+ if (!query || query.startsWith("--")) {
1079
+ return flags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
1080
+ }
1081
+ }
1082
+ if (active === "retry") {
1083
+ const flags = ["--reset-brief"];
1084
+ const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
1085
+ if (!query || query.startsWith("--")) {
1086
+ return flags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
1087
+ }
1088
+ }
1089
+ return null;
1090
+ }
1013
1091
  setupAutocomplete(fdPath) {
1014
1092
  // Define commands for autocomplete
1015
1093
  const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
@@ -1063,6 +1141,10 @@ export class InteractiveMode {
1063
1141
  if (singularCommand) {
1064
1142
  singularCommand.getArgumentCompletions = (prefix) => this.getSingularArgumentCompletions(prefix);
1065
1143
  }
1144
+ const swarmCommand = slashCommands.find((command) => command.name === "swarm");
1145
+ if (swarmCommand) {
1146
+ swarmCommand.getArgumentCompletions = (prefix) => this.getSwarmArgumentCompletions(prefix);
1147
+ }
1066
1148
  // Convert prompt templates to SlashCommand format for autocomplete
1067
1149
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
1068
1150
  name: cmd.name,
@@ -1241,6 +1323,8 @@ export class InteractiveMode {
1241
1323
  this.footerDataProvider.onBranchChange(() => {
1242
1324
  this.ui.requestRender();
1243
1325
  });
1326
+ // Refresh provider catalog from models.dev in background once per startup.
1327
+ void this.refreshModelsDevProviderCatalog();
1244
1328
  // Initialize available provider count for footer display
1245
1329
  await this.updateAvailableProviderCount();
1246
1330
  }
@@ -2581,6 +2665,11 @@ export class InteractiveMode {
2581
2665
  await this.handleOrchestrateSlashCommand(text);
2582
2666
  return;
2583
2667
  }
2668
+ if (text === "/swarm" || text.startsWith("/swarm ")) {
2669
+ this.editor.setText("");
2670
+ await this.handleSwarmCommand(text);
2671
+ return;
2672
+ }
2584
2673
  if (text === "/agents" || text.startsWith("/agents ")) {
2585
2674
  this.editor.setText("");
2586
2675
  await this.handleAgentsSlashCommand(text);
@@ -2839,6 +2928,59 @@ export class InteractiveMode {
2839
2928
  this.editor.addToHistory?.(text);
2840
2929
  };
2841
2930
  }
2931
+ updateRunningSubagentDisplay(subagent) {
2932
+ subagent.component.update({
2933
+ description: subagent.description,
2934
+ profile: subagent.profile,
2935
+ status: "running",
2936
+ phase: subagent.phase ?? "running",
2937
+ phaseState: subagent.phaseState,
2938
+ cwd: subagent.cwd,
2939
+ agent: subagent.agent,
2940
+ lockKey: subagent.lockKey,
2941
+ isolation: subagent.isolation,
2942
+ activeTool: subagent.activeTool,
2943
+ toolCallsStarted: subagent.toolCallsStarted,
2944
+ toolCallsCompleted: subagent.toolCallsCompleted,
2945
+ assistantMessages: subagent.assistantMessages,
2946
+ delegatedTasks: subagent.delegatedTasks,
2947
+ delegatedSucceeded: subagent.delegatedSucceeded,
2948
+ delegatedFailed: subagent.delegatedFailed,
2949
+ delegateIndex: subagent.delegateIndex,
2950
+ delegateTotal: subagent.delegateTotal,
2951
+ delegateDescription: subagent.delegateDescription,
2952
+ delegateProfile: subagent.delegateProfile,
2953
+ delegateItems: subagent.delegateItems,
2954
+ durationMs: Date.now() - subagent.startTime,
2955
+ });
2956
+ }
2957
+ ensureSubagentElapsedTimer() {
2958
+ if (this.subagentElapsedTimer || this.subagentComponents.size === 0) {
2959
+ return;
2960
+ }
2961
+ this.subagentElapsedTimer = setInterval(() => {
2962
+ if (this.subagentComponents.size === 0) {
2963
+ this.clearSubagentElapsedTimer();
2964
+ return;
2965
+ }
2966
+ for (const subagent of this.subagentComponents.values()) {
2967
+ this.updateRunningSubagentDisplay(subagent);
2968
+ }
2969
+ this.ui.requestRender();
2970
+ }, 1000);
2971
+ }
2972
+ clearSubagentElapsedTimer() {
2973
+ if (!this.subagentElapsedTimer) {
2974
+ return;
2975
+ }
2976
+ clearInterval(this.subagentElapsedTimer);
2977
+ this.subagentElapsedTimer = undefined;
2978
+ }
2979
+ stopSubagentElapsedTimerIfIdle() {
2980
+ if (this.subagentComponents.size === 0) {
2981
+ this.clearSubagentElapsedTimer();
2982
+ }
2983
+ }
2842
2984
  subscribeToAgent() {
2843
2985
  this.unsubscribe = this.session.subscribe(async (event) => {
2844
2986
  await this.handleEvent(event);
@@ -3020,6 +3162,7 @@ export class InteractiveMode {
3020
3162
  delegateProfile: info.delegateProfile,
3021
3163
  delegateItems: info.delegateItems,
3022
3164
  });
3165
+ this.ensureSubagentElapsedTimer();
3023
3166
  this.ui.requestRender();
3024
3167
  }
3025
3168
  else if (event.toolName !== "task" && !this.pendingTools.has(event.toolCallId)) {
@@ -3105,29 +3248,7 @@ export class InteractiveMode {
3105
3248
  subagent.phase = text.trim();
3106
3249
  }
3107
3250
  }
3108
- subagent.component.update({
3109
- description: subagent.description,
3110
- profile: subagent.profile,
3111
- status: "running",
3112
- phase: subagent.phase ?? "running",
3113
- phaseState: subagent.phaseState,
3114
- cwd: subagent.cwd,
3115
- agent: subagent.agent,
3116
- lockKey: subagent.lockKey,
3117
- isolation: subagent.isolation,
3118
- activeTool: subagent.activeTool,
3119
- toolCallsStarted: subagent.toolCallsStarted,
3120
- toolCallsCompleted: subagent.toolCallsCompleted,
3121
- assistantMessages: subagent.assistantMessages,
3122
- delegatedTasks: subagent.delegatedTasks,
3123
- delegatedSucceeded: subagent.delegatedSucceeded,
3124
- delegatedFailed: subagent.delegatedFailed,
3125
- delegateIndex: subagent.delegateIndex,
3126
- delegateTotal: subagent.delegateTotal,
3127
- delegateDescription: subagent.delegateDescription,
3128
- delegateProfile: subagent.delegateProfile,
3129
- delegateItems: subagent.delegateItems,
3130
- });
3251
+ this.updateRunningSubagentDisplay(subagent);
3131
3252
  this.ui.requestRender();
3132
3253
  break;
3133
3254
  }
@@ -3191,6 +3312,7 @@ export class InteractiveMode {
3191
3312
  this.pendingTools.delete(event.toolCallId);
3192
3313
  }
3193
3314
  this.subagentComponents.delete(event.toolCallId);
3315
+ this.stopSubagentElapsedTimerIfIdle();
3194
3316
  this.ui.requestRender();
3195
3317
  break;
3196
3318
  }
@@ -3215,6 +3337,7 @@ export class InteractiveMode {
3215
3337
  }
3216
3338
  this.pendingTools.clear();
3217
3339
  this.subagentComponents.clear();
3340
+ this.clearSubagentElapsedTimer();
3218
3341
  await this.checkShutdownRequested();
3219
3342
  this.ui.requestRender();
3220
3343
  break;
@@ -3797,7 +3920,7 @@ export class InteractiveMode {
3797
3920
  this.updateEditorBorderColor();
3798
3921
  this.refreshBuiltInHeader();
3799
3922
  const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
3800
- this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
3923
+ this.showStatus(`Switched to ${result.model.provider}/${result.model.id}${thinkingStr}`);
3801
3924
  }
3802
3925
  }
3803
3926
  catch (error) {
@@ -4837,7 +4960,7 @@ export class InteractiveMode {
4837
4960
  this.footer.invalidate();
4838
4961
  this.updateEditorBorderColor();
4839
4962
  this.refreshBuiltInHeader();
4840
- this.showStatus(`Model: ${model.id}`);
4963
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
4841
4964
  this.checkDaxnutsEasterEgg(model);
4842
4965
  }
4843
4966
  catch (error) {
@@ -4848,6 +4971,7 @@ export class InteractiveMode {
4848
4971
  this.showModelSelector(searchTerm);
4849
4972
  }
4850
4973
  async showModelProviderSelector(preferredProvider) {
4974
+ await this.hydrateMissingProviderModelsForSavedAuth();
4851
4975
  this.session.modelRegistry.refresh();
4852
4976
  let models = [];
4853
4977
  try {
@@ -4959,7 +5083,7 @@ export class InteractiveMode {
4959
5083
  this.updateEditorBorderColor();
4960
5084
  this.refreshBuiltInHeader();
4961
5085
  done();
4962
- this.showStatus(`Model: ${model.id}`);
5086
+ this.showStatus(`Model: ${model.provider}/${model.id}`);
4963
5087
  this.checkDaxnutsEasterEgg(model);
4964
5088
  }
4965
5089
  catch (error) {
@@ -5268,22 +5392,29 @@ export class InteractiveMode {
5268
5392
  return;
5269
5393
  }
5270
5394
  }
5395
+ await this.refreshModelsDevProviderCatalog();
5396
+ const apiKeyProviders = this.getApiKeyLoginProviders(this.modelsDevProviderCatalog);
5271
5397
  this.showSelector((done) => {
5272
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
5398
+ const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (provider) => {
5273
5399
  done();
5274
5400
  if (mode === "login") {
5275
- if (providerId === OPENROUTER_PROVIDER_ID) {
5276
- await this.handleOpenRouterApiKeyLogin();
5401
+ if (provider.kind === "api_key") {
5402
+ if (provider.id === OPENROUTER_PROVIDER_ID) {
5403
+ await this.handleOpenRouterApiKeyLogin();
5404
+ }
5405
+ else {
5406
+ await this.handleApiKeyLogin(provider.id, { providerName: provider.name });
5407
+ }
5277
5408
  }
5278
5409
  else {
5279
- await this.showLoginDialog(providerId);
5410
+ await this.showLoginDialog(provider.id);
5280
5411
  }
5281
5412
  }
5282
5413
  else {
5283
5414
  // Logout flow
5284
- const providerName = this.getProviderDisplayName(providerId);
5415
+ const providerName = this.getProviderDisplayName(provider.id);
5285
5416
  try {
5286
- this.session.modelRegistry.authStorage.logout(providerId);
5417
+ this.session.modelRegistry.authStorage.logout(provider.id);
5287
5418
  this.session.modelRegistry.refresh();
5288
5419
  await this.updateAvailableProviderCount();
5289
5420
  this.showStatus(`Logged out of ${providerName}`);
@@ -5295,21 +5426,156 @@ export class InteractiveMode {
5295
5426
  }, () => {
5296
5427
  done();
5297
5428
  this.ui.requestRender();
5298
- });
5429
+ }, apiKeyProviders);
5299
5430
  return { component: selector, focus: selector };
5300
5431
  });
5301
5432
  }
5433
+ async refreshModelsDevProviderCatalog() {
5434
+ if (this.modelsDevProviderCatalogRefreshPromise) {
5435
+ await this.modelsDevProviderCatalogRefreshPromise;
5436
+ return;
5437
+ }
5438
+ this.modelsDevProviderCatalogRefreshPromise = (async () => {
5439
+ const catalog = await loadModelsDevProviderCatalog();
5440
+ this.modelsDevProviderCatalogById = catalog;
5441
+ this.modelsDevProviderCatalog = Array.from(catalog.values())
5442
+ .map((provider) => ({
5443
+ id: provider.id,
5444
+ name: provider.name,
5445
+ env: provider.env,
5446
+ }))
5447
+ .sort((a, b) => a.name.localeCompare(b.name, "en") || a.id.localeCompare(b.id, "en"));
5448
+ })()
5449
+ .catch(() => {
5450
+ this.modelsDevProviderCatalog = MODELS_DEV_PROVIDERS;
5451
+ this.modelsDevProviderCatalogById = new Map(MODELS_DEV_PROVIDERS.map((provider) => [
5452
+ provider.id,
5453
+ {
5454
+ ...provider,
5455
+ models: [],
5456
+ },
5457
+ ]));
5458
+ })
5459
+ .finally(() => {
5460
+ this.modelsDevProviderCatalogRefreshPromise = undefined;
5461
+ });
5462
+ await this.modelsDevProviderCatalogRefreshPromise;
5463
+ }
5464
+ resolveModelsDevApi(modelNpm) {
5465
+ const npm = modelNpm?.toLowerCase() ?? "";
5466
+ if (npm.includes("anthropic"))
5467
+ return "anthropic-messages";
5468
+ if (npm.includes("google-vertex"))
5469
+ return "google-vertex";
5470
+ if (npm.includes("google"))
5471
+ return "google-generative-ai";
5472
+ if (npm.includes("amazon-bedrock"))
5473
+ return "bedrock-converse-stream";
5474
+ if (npm.includes("mistral"))
5475
+ return "mistral-conversations";
5476
+ if (npm.includes("@ai-sdk/openai") && !npm.includes("compatible"))
5477
+ return "openai-responses";
5478
+ return "openai-completions";
5479
+ }
5480
+ buildModelsDevProviderConfig(providerInfo) {
5481
+ const baseUrl = providerInfo.api ?? providerInfo.models.find((model) => !!model.api)?.api;
5482
+ if (!baseUrl)
5483
+ return undefined;
5484
+ if (providerInfo.models.length === 0)
5485
+ return undefined;
5486
+ const models = providerInfo.models.map((model) => ({
5487
+ id: model.id,
5488
+ name: model.name,
5489
+ api: this.resolveModelsDevApi(model.npm ?? providerInfo.npm),
5490
+ reasoning: model.reasoning,
5491
+ input: [...model.input],
5492
+ cost: model.cost,
5493
+ contextWindow: model.contextWindow,
5494
+ maxTokens: model.maxTokens,
5495
+ headers: Object.keys(model.headers).length > 0 ? model.headers : undefined,
5496
+ }));
5497
+ return {
5498
+ baseUrl,
5499
+ models,
5500
+ };
5501
+ }
5502
+ hasRegisteredProviderModels(providerId) {
5503
+ const registry = this.session.modelRegistry;
5504
+ if (typeof registry.getAll !== "function")
5505
+ return true;
5506
+ return registry.getAll().some((model) => model.provider === providerId);
5507
+ }
5508
+ async hydrateProviderModelsFromModelsDev(providerId) {
5509
+ if (this.hasRegisteredProviderModels(providerId))
5510
+ return true;
5511
+ await this.refreshModelsDevProviderCatalog();
5512
+ const providerInfo = this.modelsDevProviderCatalogById.get(providerId);
5513
+ if (!providerInfo)
5514
+ return false;
5515
+ const config = this.buildModelsDevProviderConfig(providerInfo);
5516
+ if (!config)
5517
+ return false;
5518
+ try {
5519
+ this.session.modelRegistry.registerProvider(providerId, config);
5520
+ return this.hasRegisteredProviderModels(providerId);
5521
+ }
5522
+ catch {
5523
+ return false;
5524
+ }
5525
+ }
5526
+ async hydrateMissingProviderModelsForSavedAuth() {
5527
+ const savedProviders = this.session.modelRegistry.authStorage.list();
5528
+ if (savedProviders.length === 0)
5529
+ return;
5530
+ for (const providerId of savedProviders) {
5531
+ if (this.hasRegisteredProviderModels(providerId))
5532
+ continue;
5533
+ await this.hydrateProviderModelsFromModelsDev(providerId);
5534
+ }
5535
+ }
5536
+ getApiKeyLoginProviders(modelsDevProviders) {
5537
+ const providerNames = new Map();
5538
+ this.apiKeyProviderDisplayNames.clear();
5539
+ for (const model of this.session.modelRegistry.getAll()) {
5540
+ if (!providerNames.has(model.provider)) {
5541
+ providerNames.set(model.provider, toProviderDisplayName(model.provider));
5542
+ }
5543
+ }
5544
+ for (const provider of modelsDevProviders) {
5545
+ const fallbackName = toProviderDisplayName(provider.id);
5546
+ const current = providerNames.get(provider.id);
5547
+ if (!current || current === fallbackName) {
5548
+ providerNames.set(provider.id, provider.name || fallbackName);
5549
+ }
5550
+ }
5551
+ for (const providerId of this.session.modelRegistry.authStorage.list()) {
5552
+ if (!providerNames.has(providerId)) {
5553
+ providerNames.set(providerId, toProviderDisplayName(providerId));
5554
+ }
5555
+ }
5556
+ const oauthProviderIds = new Set(this.session.modelRegistry.authStorage.getOAuthProviders().map((provider) => provider.id));
5557
+ const providers = [];
5558
+ for (const [id, name] of providerNames.entries()) {
5559
+ if (oauthProviderIds.has(id))
5560
+ continue;
5561
+ this.apiKeyProviderDisplayNames.set(id, name);
5562
+ providers.push({ id, name, kind: "api_key" });
5563
+ }
5564
+ providers.sort((a, b) => a.name.localeCompare(b.name));
5565
+ return providers;
5566
+ }
5302
5567
  getProviderDisplayName(providerId) {
5303
- if (providerId === OPENROUTER_PROVIDER_ID) {
5304
- return "OpenRouter";
5568
+ const apiKeyName = this.apiKeyProviderDisplayNames.get(providerId);
5569
+ if (apiKeyName) {
5570
+ return apiKeyName;
5305
5571
  }
5306
5572
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5307
- return providerInfo?.name || providerId;
5573
+ return providerInfo?.name || toProviderDisplayName(providerId);
5308
5574
  }
5309
- async handleOpenRouterApiKeyLogin(options) {
5575
+ async handleApiKeyLogin(providerId, options) {
5576
+ const providerName = options?.providerName || this.getProviderDisplayName(providerId);
5310
5577
  const openModelSelector = options?.openModelSelector ?? true;
5311
- const providerName = this.getProviderDisplayName(OPENROUTER_PROVIDER_ID);
5312
- const existingCredential = this.session.modelRegistry.authStorage.get(OPENROUTER_PROVIDER_ID);
5578
+ const existingCredential = this.session.modelRegistry.authStorage.get(providerId);
5313
5579
  if (existingCredential) {
5314
5580
  const overwrite = await this.showExtensionConfirm(`${providerName}: replace existing credentials?`, `Stored at ${getAuthPath()}`);
5315
5581
  if (!overwrite) {
@@ -5317,7 +5583,11 @@ export class InteractiveMode {
5317
5583
  return;
5318
5584
  }
5319
5585
  }
5320
- const keyInput = await this.showExtensionInput(`${providerName} API key\nCreate key: https://openrouter.ai/keys`, "sk-or-v1-...");
5586
+ const promptLines = [`${providerName} API key`];
5587
+ if (options?.createKeyUrl) {
5588
+ promptLines.push(`Create key: ${options.createKeyUrl}`);
5589
+ }
5590
+ const keyInput = await this.showExtensionInput(promptLines.join("\n"), options?.placeholder ?? "api-key");
5321
5591
  if (keyInput === undefined) {
5322
5592
  this.showStatus(`${providerName} login cancelled.`);
5323
5593
  return;
@@ -5327,13 +5597,28 @@ export class InteractiveMode {
5327
5597
  this.showWarning(`${providerName} API key cannot be empty.`);
5328
5598
  return;
5329
5599
  }
5330
- this.session.modelRegistry.authStorage.set(OPENROUTER_PROVIDER_ID, { type: "api_key", key: apiKey });
5600
+ this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
5601
+ let hasProviderModels = this.hasRegisteredProviderModels(providerId);
5602
+ if (!hasProviderModels) {
5603
+ hasProviderModels = await this.hydrateProviderModelsFromModelsDev(providerId);
5604
+ }
5331
5605
  await this.updateAvailableProviderCount();
5332
5606
  this.showStatus(`${providerName} API key saved to ${getAuthPath()}`);
5333
- if (openModelSelector) {
5334
- await this.showModelProviderSelector(OPENROUTER_PROVIDER_ID);
5607
+ if (openModelSelector && hasProviderModels) {
5608
+ await this.showModelProviderSelector(providerId);
5609
+ }
5610
+ else if (openModelSelector) {
5611
+ this.showWarning(`${providerName} configured, but no models are available yet. Run /model after network is available.`);
5335
5612
  }
5336
5613
  }
5614
+ async handleOpenRouterApiKeyLogin(options) {
5615
+ await this.handleApiKeyLogin(OPENROUTER_PROVIDER_ID, {
5616
+ providerName: "OpenRouter",
5617
+ openModelSelector: options?.openModelSelector,
5618
+ createKeyUrl: "https://openrouter.ai/keys",
5619
+ placeholder: "sk-or-v1-...",
5620
+ });
5621
+ }
5337
5622
  async showLoginDialog(providerId) {
5338
5623
  const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
5339
5624
  const providerName = this.getProviderDisplayName(providerId);
@@ -6838,7 +7123,105 @@ export class InteractiveMode {
6838
7123
  }
6839
7124
  return lines.length > 0 ? lines.join("\n") : "- none";
6840
7125
  }
6841
- buildSingularAgentPrompt(request, baseline, contract) {
7126
+ resolveSingularRepoScaleMode(baseline) {
7127
+ if (baseline.scannedFiles >= 8000 || baseline.sourceFiles >= 4000) {
7128
+ return {
7129
+ mode: "large",
7130
+ reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
7131
+ };
7132
+ }
7133
+ if (baseline.scannedFiles >= 2500 || baseline.sourceFiles >= 1200) {
7134
+ return {
7135
+ mode: "medium",
7136
+ reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
7137
+ };
7138
+ }
7139
+ return {
7140
+ mode: "small",
7141
+ reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
7142
+ };
7143
+ }
7144
+ buildSingularSemanticGuidanceFromStatus(status) {
7145
+ if (!status.configured) {
7146
+ return {
7147
+ statusLine: "not_configured",
7148
+ promptGuidance: [
7149
+ "Semantic index is unavailable; use narrow path-based discovery and avoid wide repo scans.",
7150
+ "If confidence is low, explicitly ask user to run /semantic setup and /semantic index, then rerun /singular.",
7151
+ ],
7152
+ operatorHint: "Large/medium repo mode: semantic index is not configured. Run /semantic setup, then /semantic index.",
7153
+ };
7154
+ }
7155
+ if (!status.enabled) {
7156
+ return {
7157
+ statusLine: "configured_but_disabled",
7158
+ promptGuidance: [
7159
+ "Semantic index is configured but disabled; proceed with targeted rg/read steps only.",
7160
+ "If discovery quality is insufficient, ask user to enable semantic search in /semantic setup.",
7161
+ ],
7162
+ operatorHint: "Large/medium repo mode: semantic index is disabled. Enable it in /semantic setup for faster planning.",
7163
+ };
7164
+ }
7165
+ if (!status.indexed) {
7166
+ return {
7167
+ statusLine: "configured_not_indexed",
7168
+ promptGuidance: [
7169
+ "Semantic index is configured but missing; do focused discovery and avoid broad scans.",
7170
+ "If context coverage is insufficient, ask user to run /semantic index before final recommendation.",
7171
+ ],
7172
+ operatorHint: "Large/medium repo mode: semantic index is missing. Run /semantic index.",
7173
+ };
7174
+ }
7175
+ if (status.stale) {
7176
+ const requiresRebuild = status.staleReason === "provider_changed" ||
7177
+ status.staleReason === "chunking_changed" ||
7178
+ status.staleReason === "index_filters_changed" ||
7179
+ status.staleReason === "dimension_mismatch";
7180
+ return {
7181
+ statusLine: `stale${status.staleReason ? ` (${status.staleReason})` : ""}`,
7182
+ promptGuidance: [
7183
+ "Semantic index is stale; treat semantic hits as hints and verify with direct file reads.",
7184
+ "If index staleness blocks confidence, ask user to run /semantic rebuild or /semantic index.",
7185
+ ],
7186
+ operatorHint: requiresRebuild
7187
+ ? "Large/medium repo mode: semantic index is stale and requires /semantic rebuild."
7188
+ : "Large/medium repo mode: semantic index is stale. Run /semantic index.",
7189
+ };
7190
+ }
7191
+ return {
7192
+ statusLine: `ready (${status.provider}/${status.model}, files=${status.files}, chunks=${status.chunks}, auto_index=${status.autoIndex ? "on" : "off"})`,
7193
+ promptGuidance: [
7194
+ "Use semantic_search for first-pass discovery, then confirm with targeted reads and grep.",
7195
+ "Avoid full-tree scans unless evidence is still insufficient.",
7196
+ ],
7197
+ };
7198
+ }
7199
+ async buildSingularSemanticGuidance(scaleMode) {
7200
+ if (scaleMode === "small") {
7201
+ return {
7202
+ statusLine: "optional_for_small_repo",
7203
+ promptGuidance: [
7204
+ "Prefer direct targeted reads/grep; semantic index is optional for this repository size.",
7205
+ ],
7206
+ };
7207
+ }
7208
+ try {
7209
+ const status = await this.createSemanticRuntime().status();
7210
+ return this.buildSingularSemanticGuidanceFromStatus(status);
7211
+ }
7212
+ catch (error) {
7213
+ const message = error instanceof Error ? error.message : String(error);
7214
+ return {
7215
+ statusLine: `status_unavailable (${message})`,
7216
+ promptGuidance: [
7217
+ "Semantic status is unavailable; proceed with conservative targeted discovery.",
7218
+ "If discovery quality is insufficient, ask user to configure /semantic setup and rerun /singular.",
7219
+ ],
7220
+ operatorHint: `Large/medium repo mode: cannot read semantic status (${message}). Run /semantic status.`,
7221
+ };
7222
+ }
7223
+ }
7224
+ buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance) {
6842
7225
  const filesHint = baseline.matchedFiles.length > 0
6843
7226
  ? baseline.matchedFiles.slice(0, 12).map((item) => `- ${item}`).join("\n")
6844
7227
  : "- no direct file matches found in heuristic pass";
@@ -6895,6 +7278,12 @@ export class InteractiveMode {
6895
7278
  `- baseline_blast_radius: ${baseline.baselineBlastRadius}`,
6896
7279
  `- baseline_recommendation: ${baseline.recommendation}`,
6897
7280
  "",
7281
+ "Repository runtime guidance:",
7282
+ `- scale_mode: ${runtimeGuidance.scaleMode}`,
7283
+ `- scale_reason: ${runtimeGuidance.scaleReason}`,
7284
+ `- semantic_status: ${runtimeGuidance.semanticStatusLine}`,
7285
+ ...runtimeGuidance.semanticGuidance.map((line) => `- ${line}`),
7286
+ "",
6898
7287
  "Matched file hints from baseline (verify, do not assume blindly):",
6899
7288
  filesHint,
6900
7289
  "",
@@ -7025,7 +7414,7 @@ export class InteractiveMode {
7025
7414
  hasRecommendation: recommendationRaw !== undefined,
7026
7415
  };
7027
7416
  }
7028
- async runSingularAgentFeasibilityPass(request, baseline, contract) {
7417
+ async runSingularAgentFeasibilityPass(request, baseline, contract, runtimeGuidance) {
7029
7418
  const model = this.session.model;
7030
7419
  if (!model) {
7031
7420
  return undefined;
@@ -7076,7 +7465,7 @@ export class InteractiveMode {
7076
7465
  });
7077
7466
  this.singularAnalysisSession = session;
7078
7467
  try {
7079
- const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract);
7468
+ const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance);
7080
7469
  const strictRetryPrompt = [
7081
7470
  "Retry strict mode:",
7082
7471
  "- Inspect repository files using tools first.",
@@ -7176,7 +7565,7 @@ export class InteractiveMode {
7176
7565
  }
7177
7566
  lines.push("");
7178
7567
  lines.push("next_action:");
7179
- lines.push(" choose option 1/2/3 to generate a detailed execution draft in editor");
7568
+ lines.push(" choose option 1/2/3, then pick Start with Swarm or Continue without Swarm");
7180
7569
  return lines.join("\n");
7181
7570
  }
7182
7571
  buildSingularExecutionDraft(result, option, contract) {
@@ -7280,10 +7669,22 @@ export class InteractiveMode {
7280
7669
  "cons:",
7281
7670
  ...picked.cons.map((item) => `- ${item}`),
7282
7671
  ].join("\n"));
7283
- if (picked.id === "3" || result.recommendation === "defer") {
7672
+ if (picked.id === "3") {
7284
7673
  this.showStatus("Singular: defer option selected, implementation postponed.");
7285
7674
  return;
7286
7675
  }
7676
+ const executionChoice = await this.showExtensionSelector("/singular: execution mode", ["Start with Swarm (Recommended)", "Continue without Swarm", "Cancel"]);
7677
+ if (!executionChoice || executionChoice === "Cancel") {
7678
+ this.showStatus("Singular: execution cancelled.");
7679
+ return;
7680
+ }
7681
+ if (executionChoice.startsWith("Start with Swarm")) {
7682
+ await this.runSwarmFromSingular({
7683
+ runId: result.runId,
7684
+ option: Number.parseInt(picked.id, 10),
7685
+ });
7686
+ return;
7687
+ }
7287
7688
  this.editor.setText(this.buildSingularExecutionDraft(result, picked, this.singularLastEffectiveContract));
7288
7689
  this.showStatus("Singular: detailed execution draft generated in editor.");
7289
7690
  }
@@ -7319,13 +7720,27 @@ export class InteractiveMode {
7319
7720
  autosave: false,
7320
7721
  contract: effectiveContract,
7321
7722
  });
7723
+ const scale = this.resolveSingularRepoScaleMode(baseline);
7724
+ const semanticGuidance = await this.buildSingularSemanticGuidance(scale.mode);
7725
+ if (scale.mode !== "small") {
7726
+ this.showStatus(`Singular scale mode: ${scale.mode} (${scale.reason})`);
7727
+ }
7728
+ if (semanticGuidance.operatorHint) {
7729
+ this.showWarning(semanticGuidance.operatorHint);
7730
+ }
7731
+ const runtimeGuidance = {
7732
+ scaleMode: scale.mode,
7733
+ scaleReason: scale.reason,
7734
+ semanticStatusLine: semanticGuidance.statusLine,
7735
+ semanticGuidance: semanticGuidance.promptGuidance,
7736
+ };
7322
7737
  let result = baseline;
7323
7738
  if (!this.session.model) {
7324
7739
  this.showWarning("No model selected. /singular used heuristic analysis only. Use /model to enable agent feasibility pass.");
7325
7740
  }
7326
7741
  else {
7327
7742
  try {
7328
- const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract);
7743
+ const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract, runtimeGuidance);
7329
7744
  if (enriched) {
7330
7745
  result = enriched;
7331
7746
  }
@@ -7362,6 +7777,9 @@ export class InteractiveMode {
7362
7777
  " /singular last",
7363
7778
  " /singular help",
7364
7779
  "",
7780
+ "Flow:",
7781
+ " /singular -> choose option -> Start with Swarm or Continue without Swarm",
7782
+ "",
7365
7783
  "Examples:",
7366
7784
  " /singular add account dashboard",
7367
7785
  " /singular introduce RBAC for API",
@@ -8206,13 +8624,28 @@ export class InteractiveMode {
8206
8624
  return;
8207
8625
  }
8208
8626
  }
8627
+ const mentionTask = cleaned.length > 0 ? cleaned : userInput;
8628
+ const orchestrationAwareAgent = /orchestrator/i.test(mentionedAgent);
8629
+ const mentionMode = orchestrationAwareAgent ? "parallel" : "sequential";
8630
+ const mentionMaxParallel = orchestrationAwareAgent ? 20 : undefined;
8209
8631
  const mentionPrompt = [
8210
- "<orchestrate mode=\"sequential\" agents=\"1\">",
8632
+ `<orchestrate mode="${mentionMode}" agents="1"${mentionMaxParallel ? ` max_parallel="${mentionMaxParallel}"` : ""}>`,
8211
8633
  `- agent 1: profile=${this.activeProfileName} cwd=${this.sessionManager.getCwd()} agent=${mentionedAgent}`,
8212
- `task: ${cleaned.length > 0 ? cleaned : userInput}`,
8634
+ `task: ${mentionTask}`,
8213
8635
  "constraints:",
8214
8636
  "- user selected a concrete custom agent via @mention",
8215
8637
  `- MUST call task tool with agent="${mentionedAgent}"`,
8638
+ ...(orchestrationAwareAgent
8639
+ ? [
8640
+ "- Include delegate_parallel_hint in the task call.",
8641
+ "- If user explicitly requested an agent count, set delegate_parallel_hint to that count (clamp 1..10).",
8642
+ "- Otherwise set delegate_parallel_hint based on complexity: simple=1, medium=3-6, complex/risky=7-10.",
8643
+ "- For non-trivial tasks, prefer delegate_parallel_hint >= 2 and split into independent <delegate_task> workstreams.",
8644
+ '- Prefer existing custom agents for delegated work when suitable (use <delegate_task agent="name" ...>).',
8645
+ "- If no existing custom agent fits, create focused delegate streams via profile-based <delegate_task> blocks.",
8646
+ "- If single-agent execution is still chosen, include one line: DELEGATION_IMPOSSIBLE: <reason>.",
8647
+ ]
8648
+ : []),
8216
8649
  "</orchestrate>",
8217
8650
  ].join("\n");
8218
8651
  await this.session.prompt(mentionPrompt, {
@@ -8385,6 +8818,1104 @@ export class InteractiveMode {
8385
8818
  }
8386
8819
  return { targetIndex, maxIterations, forceInit };
8387
8820
  }
8821
+ getSwarmHelpText() {
8822
+ return [
8823
+ "Usage:",
8824
+ " /swarm run <task> [--max-parallel N] [--budget-usd X]",
8825
+ " /swarm from-singular <run-id> --option <1|2|3> [--max-parallel N] [--budget-usd X]",
8826
+ " /swarm watch [run-id]",
8827
+ " /swarm retry <run-id> <task-id> [--reset-brief]",
8828
+ " /swarm resume <run-id>",
8829
+ " /swarm help",
8830
+ "",
8831
+ "Consistency model:",
8832
+ " Scopes -> Touches -> Locks -> Gates -> Done",
8833
+ ].join("\n");
8834
+ }
8835
+ buildSwarmRecommendationFromOrchestrate(parsed) {
8836
+ const reasons = [];
8837
+ let score = 0;
8838
+ const task = parsed.task.replace(/\s+/g, " ").trim();
8839
+ const normalizedTask = task.toLowerCase();
8840
+ const dependencyEdges = parsed.dependencies?.reduce((sum, entry) => sum + entry.dependsOn.length, 0) ?? 0;
8841
+ const effectiveParallel = parsed.mode === "parallel" ? parsed.maxParallel ?? parsed.agents : 1;
8842
+ const highRiskPattern = /\b(refactor|rewrite|migration|migrate|breaking|rollback|security|auth|authentication|authorization|permission|payment|billing|schema|database|critical)\b/i;
8843
+ const mediumRiskPattern = /\b(cross[-\s]?module|architecture|infra|platform|multi[-\s]?file|integration)\b/i;
8844
+ if (highRiskPattern.test(normalizedTask)) {
8845
+ score += 2;
8846
+ reasons.push("task has high-risk keywords");
8847
+ }
8848
+ else if (mediumRiskPattern.test(normalizedTask)) {
8849
+ score += 1;
8850
+ reasons.push("task has architecture/cross-module scope");
8851
+ }
8852
+ if (parsed.agents >= 4) {
8853
+ score += 2;
8854
+ reasons.push(`high agent count (${parsed.agents})`);
8855
+ }
8856
+ else if (parsed.agents >= 3) {
8857
+ score += 1;
8858
+ reasons.push(`multi-agent run (${parsed.agents})`);
8859
+ }
8860
+ if (dependencyEdges >= 3) {
8861
+ score += 2;
8862
+ reasons.push(`complex dependency graph (${dependencyEdges} edges)`);
8863
+ }
8864
+ else if (dependencyEdges > 0) {
8865
+ score += 1;
8866
+ reasons.push(`dependency graph present (${dependencyEdges} edges)`);
8867
+ }
8868
+ if (effectiveParallel >= 3) {
8869
+ score += 1;
8870
+ reasons.push(`high parallelism (${effectiveParallel})`);
8871
+ }
8872
+ if ((parsed.locks?.length ?? 0) > 0) {
8873
+ score += 1;
8874
+ reasons.push("explicit lock coordination requested");
8875
+ }
8876
+ if (task.length >= 180) {
8877
+ score += 1;
8878
+ reasons.push("long task brief");
8879
+ }
8880
+ const safeTask = task.replace(/"/g, "'");
8881
+ const commandParts = [`/swarm run "${safeTask}"`];
8882
+ if (parsed.maxParallel !== undefined && parsed.maxParallel > 0) {
8883
+ commandParts.push(`--max-parallel ${parsed.maxParallel}`);
8884
+ }
8885
+ return {
8886
+ recommend: score >= 3,
8887
+ reasons,
8888
+ command: commandParts.join(" "),
8889
+ };
8890
+ }
8891
+ isEffectiveContractReady(contract) {
8892
+ const hasText = (value) => typeof value === "string" && value.trim().length > 0;
8893
+ const hasList = (value) => Array.isArray(value) && value.some((item) => item.trim().length > 0);
8894
+ return (hasText(contract.goal) ||
8895
+ hasList(contract.scope_include) ||
8896
+ hasList(contract.constraints) ||
8897
+ hasList(contract.quality_gates) ||
8898
+ hasList(contract.definition_of_done));
8899
+ }
8900
+ parseContractListInput(raw) {
8901
+ if (!raw)
8902
+ return [];
8903
+ return raw
8904
+ .split(/[\n;,]+/)
8905
+ .map((item) => item.trim())
8906
+ .filter((item) => item.length > 0);
8907
+ }
8908
+ buildAutoDraftContractFromTask(task) {
8909
+ const normalizedTask = task.replace(/\s+/g, " ").trim();
8910
+ const hints = loadProjectIndex(this.sessionManager.getCwd());
8911
+ const matchedFiles = hints ? queryProjectIndex(hints, task, 8).matches.map((entry) => entry.path) : [];
8912
+ const scopeInclude = matchedFiles.length > 0 ? matchedFiles.map((filePath) => filePath.split("/").slice(0, 2).join("/")).slice(0, 6) : [];
8913
+ return normalizeEngineeringContract({
8914
+ goal: normalizedTask.length > 0 ? normalizedTask : "Deliver requested change safely with bounded blast radius.",
8915
+ scope_include: scopeInclude.length > 0 ? [...new Set(scopeInclude)].map((scope) => `${scope}/**`) : ["src/**", "test/**"],
8916
+ scope_exclude: ["node_modules/**", "dist/**", ".iosm/**"],
8917
+ constraints: [
8918
+ "Preserve backward compatibility unless explicitly approved.",
8919
+ "Keep changes scoped to declared touch zones.",
8920
+ ],
8921
+ quality_gates: [
8922
+ "Targeted tests for touched modules pass.",
8923
+ "Lint/type checks pass for changed files.",
8924
+ ],
8925
+ definition_of_done: [
8926
+ "Implementation merged with verification evidence.",
8927
+ "Risk notes and rollback path documented.",
8928
+ ],
8929
+ });
8930
+ }
8931
+ async runSwarmContractGuidedInterview(task) {
8932
+ const goal = await this.showExtensionInput("Swarm contract: goal (required)", task.trim() || "Deliver requested change safely.");
8933
+ if (goal === undefined)
8934
+ return undefined;
8935
+ const scopeInclude = await this.showExtensionInput("Swarm contract: scope_include (comma/newline separated)", "src/**, test/**");
8936
+ if (scopeInclude === undefined)
8937
+ return undefined;
8938
+ const scopeExclude = await this.showExtensionInput("Swarm contract: scope_exclude (comma/newline separated)", "node_modules/**, dist/**, .iosm/**");
8939
+ if (scopeExclude === undefined)
8940
+ return undefined;
8941
+ const constraints = await this.showExtensionInput("Swarm contract: constraints (comma/newline separated)", "no breaking API changes; no unrelated refactors");
8942
+ if (constraints === undefined)
8943
+ return undefined;
8944
+ const gates = await this.showExtensionInput("Swarm contract: quality_gates (comma/newline separated)", "targeted tests pass; lint/type checks pass");
8945
+ if (gates === undefined)
8946
+ return undefined;
8947
+ const done = await this.showExtensionInput("Swarm contract: definition_of_done (comma/newline separated)", "implementation complete; validation evidence attached");
8948
+ if (done === undefined)
8949
+ return undefined;
8950
+ return normalizeEngineeringContract({
8951
+ goal: goal.trim(),
8952
+ scope_include: this.parseContractListInput(scopeInclude),
8953
+ scope_exclude: this.parseContractListInput(scopeExclude),
8954
+ constraints: this.parseContractListInput(constraints),
8955
+ quality_gates: this.parseContractListInput(gates),
8956
+ definition_of_done: this.parseContractListInput(done),
8957
+ });
8958
+ }
8959
+ async ensureSwarmEffectiveContract(task) {
8960
+ const initialState = this.getContractStateSafe();
8961
+ if (!initialState)
8962
+ return undefined;
8963
+ if (this.isEffectiveContractReady(initialState.effective)) {
8964
+ return initialState.effective;
8965
+ }
8966
+ while (true) {
8967
+ const selected = await this.showExtensionSelector([
8968
+ "/swarm requires an effective /contract before execution can start.",
8969
+ "Choose how to bootstrap contract policy:",
8970
+ ].join("\n"), [
8971
+ "Auto-draft from task (Recommended)",
8972
+ "Guided Q&A",
8973
+ "Open manual /contract editor",
8974
+ "Cancel",
8975
+ ]);
8976
+ if (!selected || selected === "Cancel") {
8977
+ this.showStatus("Swarm cancelled: contract bootstrap not completed.");
8978
+ return undefined;
8979
+ }
8980
+ if (selected.startsWith("Auto-draft")) {
8981
+ try {
8982
+ const draft = this.buildAutoDraftContractFromTask(task);
8983
+ this.contractService.save("session", draft);
8984
+ this.syncRuntimePromptSuffix();
8985
+ this.showStatus("Swarm contract bootstrap: auto-draft saved to session overlay.");
8986
+ }
8987
+ catch (error) {
8988
+ this.showWarning(error instanceof Error ? error.message : String(error));
8989
+ }
8990
+ }
8991
+ else if (selected === "Guided Q&A") {
8992
+ const drafted = await this.runSwarmContractGuidedInterview(task);
8993
+ if (!drafted) {
8994
+ this.showStatus("Swarm contract interview cancelled.");
8995
+ }
8996
+ else {
8997
+ try {
8998
+ this.contractService.save("session", drafted);
8999
+ this.syncRuntimePromptSuffix();
9000
+ this.showStatus("Swarm contract bootstrap: guided contract saved to session overlay.");
9001
+ }
9002
+ catch (error) {
9003
+ this.showWarning(error instanceof Error ? error.message : String(error));
9004
+ }
9005
+ }
9006
+ }
9007
+ else if (selected === "Open manual /contract editor") {
9008
+ await this.runContractInteractiveMenu();
9009
+ }
9010
+ const state = this.getContractStateSafe();
9011
+ if (!state)
9012
+ return undefined;
9013
+ if (this.isEffectiveContractReady(state.effective)) {
9014
+ return state.effective;
9015
+ }
9016
+ this.showWarning("Effective contract is still empty. Swarm execution remains blocked.");
9017
+ }
9018
+ }
9019
+ async maybeWarnSwarmSemantic(scaleMode) {
9020
+ if (scaleMode === "small") {
9021
+ return "optional_for_small_repo";
9022
+ }
9023
+ try {
9024
+ const status = await this.createSemanticRuntime().status();
9025
+ if (!status.configured) {
9026
+ this.showWarning("Swarm recommendation: configure semantic index via /semantic setup and /semantic index.");
9027
+ return "not_configured";
9028
+ }
9029
+ if (!status.enabled) {
9030
+ this.showWarning("Swarm recommendation: enable semantic index in /semantic setup for medium/large repositories.");
9031
+ return "configured_but_disabled";
9032
+ }
9033
+ if (!status.indexed) {
9034
+ this.showWarning("Swarm recommendation: run /semantic index before long swarm runs.");
9035
+ return "configured_not_indexed";
9036
+ }
9037
+ if (status.stale) {
9038
+ const action = status.staleReason === "provider_changed" || status.staleReason === "dimension_mismatch" ? "/semantic rebuild" : "/semantic index";
9039
+ this.showWarning(`Swarm recommendation: semantic index is stale (${status.staleReason ?? "unknown"}). Run ${action}.`);
9040
+ return `stale:${status.staleReason ?? "unknown"}`;
9041
+ }
9042
+ return `ready:${status.provider}/${status.model}`;
9043
+ }
9044
+ catch (error) {
9045
+ const message = error instanceof Error ? error.message : String(error);
9046
+ this.showWarning(`Swarm recommendation: semantic status unavailable (${message}). Use /semantic status.`);
9047
+ return `status_unavailable:${message}`;
9048
+ }
9049
+ }
9050
+ ensureSwarmProjectIndex(task) {
9051
+ const cwd = this.sessionManager.getCwd();
9052
+ const existing = loadProjectIndex(cwd);
9053
+ if (existing) {
9054
+ if (existing.meta.repoScaleMode === "small") {
9055
+ return { index: existing, scaleMode: "small", rebuilt: false };
9056
+ }
9057
+ const ensured = ensureProjectIndex(cwd, existing.meta.repoScaleMode);
9058
+ return { index: ensured.index, scaleMode: ensured.index.meta.repoScaleMode, rebuilt: ensured.rebuilt };
9059
+ }
9060
+ const quick = buildProjectIndex(cwd, { maxFiles: 6_000 });
9061
+ const scaleMode = quick.meta.repoScaleMode;
9062
+ if (scaleMode === "small") {
9063
+ return { index: quick, scaleMode, rebuilt: true };
9064
+ }
9065
+ saveProjectIndex(cwd, quick);
9066
+ return { index: quick, scaleMode, rebuilt: true };
9067
+ }
9068
+ parseSwarmCommand(text) {
9069
+ const args = this.parseSlashArgs(text).slice(1);
9070
+ if (args.length === 0) {
9071
+ return { subcommand: "help" };
9072
+ }
9073
+ const subcommand = (args[0] ?? "").toLowerCase();
9074
+ const rest = args.slice(1);
9075
+ if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
9076
+ return { subcommand: "help" };
9077
+ }
9078
+ if (subcommand === "watch") {
9079
+ return {
9080
+ subcommand: "watch",
9081
+ runId: rest[0],
9082
+ };
9083
+ }
9084
+ if (subcommand === "resume") {
9085
+ const runId = rest[0];
9086
+ if (!runId) {
9087
+ this.showWarning("Usage: /swarm resume <run-id>");
9088
+ return undefined;
9089
+ }
9090
+ return { subcommand: "resume", runId };
9091
+ }
9092
+ if (subcommand === "retry") {
9093
+ const runId = rest[0];
9094
+ const taskId = rest[1];
9095
+ if (!runId || !taskId) {
9096
+ this.showWarning("Usage: /swarm retry <run-id> <task-id> [--reset-brief]");
9097
+ return undefined;
9098
+ }
9099
+ const resetBrief = rest.slice(2).some((token) => token === "--reset-brief");
9100
+ return { subcommand: "retry", runId, taskId, resetBrief };
9101
+ }
9102
+ let maxParallel;
9103
+ let budgetUsd;
9104
+ const taskParts = [];
9105
+ let fromSingularOption;
9106
+ let fromSingularRunId;
9107
+ for (let index = 0; index < rest.length; index += 1) {
9108
+ const token = rest[index] ?? "";
9109
+ if (token === "--max-parallel") {
9110
+ const next = rest[index + 1];
9111
+ const parsed = next ? Number.parseInt(next, 10) : Number.NaN;
9112
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
9113
+ this.showWarning("Invalid --max-parallel value (expected 1..20).");
9114
+ return undefined;
9115
+ }
9116
+ maxParallel = parsed;
9117
+ index += 1;
9118
+ continue;
9119
+ }
9120
+ if (token === "--budget-usd") {
9121
+ const next = rest[index + 1];
9122
+ const parsed = next ? Number.parseFloat(next) : Number.NaN;
9123
+ if (!Number.isFinite(parsed) || parsed <= 0) {
9124
+ this.showWarning("Invalid --budget-usd value (expected > 0).");
9125
+ return undefined;
9126
+ }
9127
+ budgetUsd = parsed;
9128
+ index += 1;
9129
+ continue;
9130
+ }
9131
+ if (token === "--option") {
9132
+ const next = rest[index + 1];
9133
+ const parsed = next ? Number.parseInt(next, 10) : Number.NaN;
9134
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 3) {
9135
+ this.showWarning("Invalid --option value (expected 1|2|3).");
9136
+ return undefined;
9137
+ }
9138
+ fromSingularOption = parsed;
9139
+ index += 1;
9140
+ continue;
9141
+ }
9142
+ if (token.startsWith("-")) {
9143
+ this.showWarning(`Unknown option for /swarm ${subcommand}: ${token}`);
9144
+ return undefined;
9145
+ }
9146
+ if (subcommand === "from-singular" && !fromSingularRunId) {
9147
+ fromSingularRunId = token;
9148
+ continue;
9149
+ }
9150
+ taskParts.push(token);
9151
+ }
9152
+ if (subcommand === "run") {
9153
+ const task = taskParts.join(" ").trim();
9154
+ if (!task) {
9155
+ this.showWarning("Usage: /swarm run <task> [--max-parallel N] [--budget-usd X]");
9156
+ return undefined;
9157
+ }
9158
+ return { subcommand, task, maxParallel, budgetUsd };
9159
+ }
9160
+ if (subcommand === "from-singular") {
9161
+ if (!fromSingularRunId || !fromSingularOption) {
9162
+ this.showWarning("Usage: /swarm from-singular <run-id> --option <1|2|3> [--max-parallel N] [--budget-usd X]");
9163
+ return undefined;
9164
+ }
9165
+ return {
9166
+ subcommand,
9167
+ runId: fromSingularRunId,
9168
+ option: fromSingularOption,
9169
+ maxParallel,
9170
+ budgetUsd,
9171
+ };
9172
+ }
9173
+ this.showWarning(`Unknown /swarm subcommand "${subcommand}". Use /swarm help.`);
9174
+ return undefined;
9175
+ }
9176
+ buildSwarmBootstrapState(runId, plan, budgetUsd) {
9177
+ const tasks = {};
9178
+ for (const task of plan.tasks) {
9179
+ tasks[task.id] = {
9180
+ id: task.id,
9181
+ status: task.depends_on.length === 0 ? "ready" : "pending",
9182
+ attempts: 0,
9183
+ depends_on: [...task.depends_on],
9184
+ touches: [...task.touches],
9185
+ scopes: [...task.scopes],
9186
+ };
9187
+ }
9188
+ return {
9189
+ runId,
9190
+ status: "running",
9191
+ createdAt: new Date().toISOString(),
9192
+ updatedAt: new Date().toISOString(),
9193
+ tick: 0,
9194
+ noProgressTicks: 0,
9195
+ readyQueue: Object.values(tasks)
9196
+ .filter((task) => task.status === "ready")
9197
+ .map((task) => task.id),
9198
+ blockedTasks: [],
9199
+ tasks,
9200
+ locks: {},
9201
+ retries: {},
9202
+ budget: {
9203
+ limitUsd: budgetUsd,
9204
+ spentUsd: 0,
9205
+ warned80: false,
9206
+ hardStopped: false,
9207
+ },
9208
+ };
9209
+ }
9210
+ buildSwarmRunMeta(input) {
9211
+ return {
9212
+ runId: input.runId,
9213
+ createdAt: new Date().toISOString(),
9214
+ source: input.source,
9215
+ request: input.request,
9216
+ contract: input.contract,
9217
+ contractHash: crypto.createHash("sha256").update(JSON.stringify(input.contract)).digest("hex").slice(0, 16),
9218
+ repoScaleMode: input.repoScaleMode,
9219
+ semanticStatus: input.semanticStatus,
9220
+ maxParallel: input.maxParallel,
9221
+ budgetUsd: input.budgetUsd,
9222
+ linkedSingularRunId: input.linkedSingularRunId,
9223
+ linkedSingularOption: input.linkedSingularOption,
9224
+ };
9225
+ }
9226
+ resolveSwarmTaskProfile(task) {
9227
+ if (task.concurrency_class === "analysis")
9228
+ return "explore";
9229
+ if (task.concurrency_class === "verification" || task.concurrency_class === "tests")
9230
+ return "iosm_verifier";
9231
+ if (task.concurrency_class === "docs")
9232
+ return "plan";
9233
+ return "full";
9234
+ }
9235
+ estimateSwarmTaskCostUsd(task) {
9236
+ if (task.severity === "high")
9237
+ return 0.35;
9238
+ if (task.severity === "medium")
9239
+ return 0.2;
9240
+ return 0.12;
9241
+ }
9242
+ deriveSwarmTaskDelegateParallelHint(task) {
9243
+ const brief = task.brief.toLowerCase();
9244
+ const hasComplexKeyword = /(refactor|migration|rewrite|split|cross[-\s]?module|architecture|platform|integration|security|auth)/i.test(brief);
9245
+ const hasVeryComplexSignal = /(overhaul|major|system-wide|cross-cutting|multi-service|facet|registry)/i.test(brief) ||
9246
+ task.touches.length >= 5 ||
9247
+ task.scopes.length >= 4;
9248
+ if (task.severity === "low" && task.touches.length <= 2 && task.scopes.length <= 2 && !hasComplexKeyword) {
9249
+ return 1;
9250
+ }
9251
+ if (task.severity === "high" && (hasVeryComplexSignal || hasComplexKeyword || task.touches.length >= 3)) {
9252
+ return 10;
9253
+ }
9254
+ if (task.severity === "high")
9255
+ return 8;
9256
+ if (task.severity === "medium" && (hasVeryComplexSignal || task.touches.length >= 4 || hasComplexKeyword))
9257
+ return 7;
9258
+ if (task.severity === "medium")
9259
+ return 5;
9260
+ return hasComplexKeyword ? 3 : 1;
9261
+ }
9262
+ parseSwarmSpawnCandidates(output, parentTaskId) {
9263
+ const lines = output.split(/\r?\n/);
9264
+ const results = [];
9265
+ for (const rawLine of lines) {
9266
+ const line = rawLine.trim();
9267
+ const match = line.match(/^[*-]\s+(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(low|medium|high)\s*$/i);
9268
+ if (!match)
9269
+ continue;
9270
+ results.push({
9271
+ description: match[1].trim(),
9272
+ path: match[2].trim(),
9273
+ changeType: match[3].trim(),
9274
+ severity: match[4].trim().toLowerCase(),
9275
+ parentTaskId,
9276
+ });
9277
+ if (results.length >= 10)
9278
+ break;
9279
+ }
9280
+ return results;
9281
+ }
9282
+ loadSingularAnalysisByRunId(runId) {
9283
+ const analysisPath = path.join(this.singularService.getAnalysesRoot(), runId, "analysis.json");
9284
+ if (!fs.existsSync(analysisPath))
9285
+ return undefined;
9286
+ try {
9287
+ return JSON.parse(fs.readFileSync(analysisPath, "utf8"));
9288
+ }
9289
+ catch {
9290
+ return undefined;
9291
+ }
9292
+ }
9293
+ async dispatchSwarmTaskWithAgent(input) {
9294
+ const model = this.session.model;
9295
+ if (!model) {
9296
+ return {
9297
+ taskId: input.task.id,
9298
+ status: "error",
9299
+ error: "No active model configured for swarm task dispatch.",
9300
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9301
+ };
9302
+ }
9303
+ const profile = this.resolveSwarmTaskProfile(input.task);
9304
+ const delegateParallelHint = this.deriveSwarmTaskDelegateParallelHint(input.task);
9305
+ const safeDescription = input.task.brief.replace(/\s+/g, " ").trim().slice(0, 120).replace(/"/g, "'");
9306
+ const prompt = [
9307
+ `<swarm_task run_id="${input.meta.runId}" task_id="${input.task.id}" profile_hint="${profile}">`,
9308
+ `request: ${input.meta.request}`,
9309
+ `task_brief: ${input.task.brief}`,
9310
+ `touches: ${input.runtime.touches.join(", ") || "(none)"}`,
9311
+ `scopes: ${input.runtime.scopes.join(", ") || "(none)"}`,
9312
+ `constraints: ${(input.contract.constraints ?? []).join("; ") || "(none)"}`,
9313
+ `quality_gates: ${(input.contract.quality_gates ?? []).join("; ") || "(none)"}`,
9314
+ "",
9315
+ "Execution requirements:",
9316
+ `- 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}".`,
9317
+ `- Inside this single task call, prefer decomposition into <delegate_task> subtasks when delegate_parallel_hint >= 2 (target parallel fan-out up to ${delegateParallelHint}).`,
9318
+ "- If decomposition is not beneficial, continue with single-agent execution (optional note: DELEGATION_IMPOSSIBLE: <reason>).",
9319
+ "- Keep edits inside declared scopes/touches. If scope expansion is required, explain and stop.",
9320
+ "- If blocked, respond with line: BLOCKED: <reason>",
9321
+ "- Optional spawn candidates format: '- <description> | <path> | <change_type> | <low|medium|high>'",
9322
+ "</swarm_task>",
9323
+ ].join("\n");
9324
+ const cwd = this.sessionManager.getCwd();
9325
+ const agentDir = getAgentDir();
9326
+ const settingsManager = SettingsManager.create(cwd, agentDir);
9327
+ const authStorage = this.session.modelRegistry.authStorage;
9328
+ const resourceLoader = new DefaultResourceLoader({
9329
+ cwd,
9330
+ agentDir,
9331
+ settingsManager,
9332
+ noExtensions: true,
9333
+ noSkills: true,
9334
+ noPromptTemplates: true,
9335
+ });
9336
+ try {
9337
+ await resourceLoader.reload();
9338
+ }
9339
+ catch (error) {
9340
+ return {
9341
+ taskId: input.task.id,
9342
+ status: "error",
9343
+ error: `Failed to initialize swarm task runtime: ${error instanceof Error ? error.message : String(error)}`,
9344
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9345
+ };
9346
+ }
9347
+ let swarmSession;
9348
+ try {
9349
+ const created = await createAgentSession({
9350
+ cwd,
9351
+ sessionManager: SessionManager.inMemory(),
9352
+ settingsManager,
9353
+ authStorage,
9354
+ modelRegistry: this.session.modelRegistry,
9355
+ resourceLoader,
9356
+ model,
9357
+ thinkingLevel: this.session.thinkingLevel,
9358
+ profile: "full",
9359
+ enableTaskTool: true,
9360
+ });
9361
+ swarmSession = created.session;
9362
+ }
9363
+ catch (error) {
9364
+ return {
9365
+ taskId: input.task.id,
9366
+ status: "error",
9367
+ error: `Failed to create isolated swarm session: ${error instanceof Error ? error.message : String(error)}`,
9368
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9369
+ };
9370
+ }
9371
+ let taskToolCalls = 0;
9372
+ const taskErrors = [];
9373
+ let delegatedFailed = 0;
9374
+ let delegatedTasks = 0;
9375
+ const delegatedFailureCauses = new Map();
9376
+ const accumulateFailureCauses = (raw) => {
9377
+ if (!raw || typeof raw !== "object")
9378
+ return;
9379
+ for (const [cause, count] of Object.entries(raw)) {
9380
+ if (typeof cause !== "string" || !cause.trim())
9381
+ continue;
9382
+ const numeric = typeof count === "number" ? count : Number.parseInt(String(count ?? 0), 10);
9383
+ if (!Number.isFinite(numeric) || numeric <= 0)
9384
+ continue;
9385
+ delegatedFailureCauses.set(cause, (delegatedFailureCauses.get(cause) ?? 0) + numeric);
9386
+ }
9387
+ };
9388
+ const chunks = [];
9389
+ const unsubscribe = swarmSession.subscribe((event) => {
9390
+ if (event.type === "tool_execution_start" && event.toolName === "task") {
9391
+ taskToolCalls += 1;
9392
+ return;
9393
+ }
9394
+ if (event.type === "tool_execution_end" && event.toolName === "task") {
9395
+ const result = event.result;
9396
+ const details = result?.details;
9397
+ if (typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)) {
9398
+ delegatedFailed += Math.max(0, details.delegatedFailed);
9399
+ }
9400
+ if (typeof details?.delegatedTasks === "number" && Number.isFinite(details.delegatedTasks)) {
9401
+ delegatedTasks += Math.max(0, details.delegatedTasks);
9402
+ }
9403
+ accumulateFailureCauses(details?.failureCauses);
9404
+ if (event.isError) {
9405
+ taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
9406
+ }
9407
+ return;
9408
+ }
9409
+ if (event.type === "message_end" && event.message.role === "assistant") {
9410
+ for (const part of event.message.content) {
9411
+ if (part.type === "text" && part.text.trim()) {
9412
+ chunks.push(part.text.trim());
9413
+ }
9414
+ }
9415
+ }
9416
+ });
9417
+ try {
9418
+ await swarmSession.prompt(prompt, {
9419
+ expandPromptTemplates: false,
9420
+ skipIosmAutopilot: true,
9421
+ skipOrchestrationDirective: true,
9422
+ source: "interactive",
9423
+ });
9424
+ }
9425
+ catch (error) {
9426
+ return {
9427
+ taskId: input.task.id,
9428
+ status: "error",
9429
+ error: error instanceof Error ? error.message : String(error),
9430
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9431
+ };
9432
+ }
9433
+ finally {
9434
+ unsubscribe();
9435
+ if (swarmSession.isStreaming) {
9436
+ await swarmSession.abort().catch(() => {
9437
+ // best effort
9438
+ });
9439
+ }
9440
+ swarmSession.dispose();
9441
+ }
9442
+ const output = chunks.join("\n\n").trim();
9443
+ if (/^\s*BLOCKED\s*:/im.test(output)) {
9444
+ const reason = output.match(/^\s*BLOCKED\s*:\s*(.+)$/im)?.[1]?.trim() ?? "Blocked by execution policy.";
9445
+ return {
9446
+ taskId: input.task.id,
9447
+ status: "blocked",
9448
+ error: reason,
9449
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9450
+ };
9451
+ }
9452
+ if (taskToolCalls === 0) {
9453
+ return {
9454
+ taskId: input.task.id,
9455
+ status: "error",
9456
+ error: "No task tool call executed by assistant.",
9457
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9458
+ };
9459
+ }
9460
+ if (taskErrors.length > 0) {
9461
+ return {
9462
+ taskId: input.task.id,
9463
+ status: "error",
9464
+ error: taskErrors.join(" | "),
9465
+ failureCause: "task_tool_error",
9466
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9467
+ };
9468
+ }
9469
+ if (delegatedFailed > 0) {
9470
+ const failureSummary = [...delegatedFailureCauses.entries()]
9471
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
9472
+ .map(([cause, count]) => `${cause}=${count}`)
9473
+ .join(", ");
9474
+ const totalDelegates = delegatedTasks > 0 ? delegatedTasks : delegatedFailed;
9475
+ const error = `delegates_failed ${delegatedFailed}/${totalDelegates}${failureSummary ? ` (${failureSummary})` : ""}`;
9476
+ return {
9477
+ taskId: input.task.id,
9478
+ status: "error",
9479
+ error,
9480
+ failureCause: failureSummary || "delegates_failed",
9481
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9482
+ };
9483
+ }
9484
+ return {
9485
+ taskId: input.task.id,
9486
+ status: "done",
9487
+ costUsd: this.estimateSwarmTaskCostUsd(input.task),
9488
+ spawnCandidates: this.parseSwarmSpawnCandidates(output, input.task.id),
9489
+ };
9490
+ }
9491
+ async executeSwarmRun(input) {
9492
+ const cwd = this.sessionManager.getCwd();
9493
+ const store = new SwarmStateStore(cwd, input.runId);
9494
+ const initialState = input.resumeState ?? this.buildSwarmBootstrapState(input.runId, input.plan, input.budgetUsd);
9495
+ if (!input.resumeState) {
9496
+ store.init(input.meta, input.plan, initialState);
9497
+ }
9498
+ let rollingIndex = input.projectIndex;
9499
+ let localStopRequested = false;
9500
+ const refreshIncrementalIndex = () => {
9501
+ if (!input.enableIncrementalIndex || !rollingIndex)
9502
+ return;
9503
+ const changed = collectChangedFilesSince(rollingIndex, cwd);
9504
+ if (changed.length === 0)
9505
+ return;
9506
+ const rebuilt = buildProjectIndex(cwd, {
9507
+ incrementalFrom: rollingIndex,
9508
+ changedFiles: changed,
9509
+ maxFiles: Math.max(2_000, rollingIndex.meta.totalFiles + 1_000),
9510
+ });
9511
+ saveProjectIndex(cwd, rebuilt);
9512
+ rollingIndex = rebuilt;
9513
+ };
9514
+ this.swarmActiveRunId = input.runId;
9515
+ this.showStatus(`Swarm run started: ${input.runId}`);
9516
+ const schedulerResult = await runSwarmScheduler({
9517
+ runId: input.runId,
9518
+ plan: input.plan,
9519
+ contract: input.contract,
9520
+ maxParallel: input.meta.maxParallel,
9521
+ budgetUsd: input.budgetUsd,
9522
+ existingState: initialState,
9523
+ dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
9524
+ meta: input.meta,
9525
+ task,
9526
+ runtime,
9527
+ contract: input.contract,
9528
+ }),
9529
+ confirmSpawn: async ({ candidate, parentTask }) => {
9530
+ const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
9531
+ if (!requiresConfirmation)
9532
+ return true;
9533
+ const choice = await this.showExtensionSelector([
9534
+ `/swarm spawn candidate requires confirmation`,
9535
+ `severity=${candidate.severity} task=${parentTask.id}`,
9536
+ `description=${candidate.description}`,
9537
+ `path=${candidate.path}`,
9538
+ ].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
9539
+ if (!choice || choice.startsWith("Reject")) {
9540
+ this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
9541
+ return false;
9542
+ }
9543
+ if (choice === "Abort run") {
9544
+ localStopRequested = true;
9545
+ this.showWarning("Swarm run marked to stop after current scheduling step.");
9546
+ return false;
9547
+ }
9548
+ return true;
9549
+ },
9550
+ onEvent: (event) => {
9551
+ store.appendEvent(event);
9552
+ },
9553
+ onStateChanged: (state) => {
9554
+ store.saveState(state);
9555
+ store.saveCheckpoint(state);
9556
+ refreshIncrementalIndex();
9557
+ },
9558
+ shouldStop: () => this.shutdownRequested || localStopRequested,
9559
+ });
9560
+ this.swarmActiveRunId = undefined;
9561
+ const taskStates = Object.values(schedulerResult.state.tasks);
9562
+ const doneCount = taskStates.filter((task) => task.status === "done").length;
9563
+ const errorCount = taskStates.filter((task) => task.status === "error").length;
9564
+ const blockedCount = taskStates.filter((task) => task.status === "blocked").length;
9565
+ const total = taskStates.length;
9566
+ const reportLines = [
9567
+ "# Swarm Integration Report",
9568
+ "",
9569
+ `- run_id: ${input.runId}`,
9570
+ `- source: ${input.meta.source}`,
9571
+ `- request: ${input.meta.request}`,
9572
+ `- status: ${schedulerResult.state.status}`,
9573
+ `- tasks: ${doneCount}/${total} done, ${errorCount} error, ${blockedCount} blocked`,
9574
+ `- budget: ${schedulerResult.state.budget.spentUsd.toFixed(2)}${input.meta.budgetUsd ? ` / ${input.meta.budgetUsd.toFixed(2)}` : ""} USD`,
9575
+ "",
9576
+ "## Consistency Model",
9577
+ "Scopes -> Touches -> Locks -> Gates -> Done",
9578
+ "",
9579
+ "## Task Gates",
9580
+ ...schedulerResult.taskGates.map((gate) => `- ${gate.taskId}: ${gate.pass ? "pass" : "fail"}${gate.failures.length > 0 ? ` (${gate.failures.join("; ")})` : ""}`),
9581
+ "",
9582
+ "## Run Gates",
9583
+ `- pass: ${schedulerResult.runGate.pass}`,
9584
+ ...schedulerResult.runGate.failures.map((item) => `- fail: ${item}`),
9585
+ ...schedulerResult.runGate.warnings.map((item) => `- warn: ${item}`),
9586
+ "",
9587
+ "## Spawn Backlog",
9588
+ ...(schedulerResult.spawnBacklog.length > 0
9589
+ ? schedulerResult.spawnBacklog.map((item) => `- ${item.description} | ${item.path} | ${item.changeType} | fp=${item.fingerprint}`)
9590
+ : ["- none"]),
9591
+ ];
9592
+ store.writeReports({
9593
+ integrationReport: reportLines.join("\n"),
9594
+ gates: {
9595
+ task_gates: schedulerResult.taskGates,
9596
+ run_gate: schedulerResult.runGate,
9597
+ status: schedulerResult.state.status,
9598
+ },
9599
+ sharedContext: [
9600
+ "# Shared Context",
9601
+ "",
9602
+ `Run ${input.runId} finished with status: ${schedulerResult.state.status}.`,
9603
+ `Recommendation: ${schedulerResult.runGate.pass ? "proceed to /iosm for measurable optimization" : "resolve failed gates before /iosm"}.`,
9604
+ ].join("\n"),
9605
+ });
9606
+ this.showCommandTextBlock("Swarm Run", [
9607
+ `run_id: ${input.runId}`,
9608
+ `status: ${schedulerResult.state.status}`,
9609
+ `tasks: ${doneCount}/${total} done · ${errorCount} error · ${blockedCount} blocked`,
9610
+ `budget_usd: ${schedulerResult.state.budget.spentUsd.toFixed(2)}${input.meta.budgetUsd ? `/${input.meta.budgetUsd.toFixed(2)}` : ""}`,
9611
+ `watch: /swarm watch ${input.runId}`,
9612
+ `resume: /swarm resume ${input.runId}`,
9613
+ ].join("\n"));
9614
+ }
9615
+ async runSwarmFromTask(task, options) {
9616
+ const contract = await this.ensureSwarmEffectiveContract(task);
9617
+ if (!contract)
9618
+ return;
9619
+ const indexInfo = this.ensureSwarmProjectIndex(task);
9620
+ if (indexInfo.rebuilt) {
9621
+ this.showStatus(`Swarm project index ready (${indexInfo.scaleMode}).`);
9622
+ }
9623
+ const semanticStatus = await this.maybeWarnSwarmSemantic(indexInfo.scaleMode);
9624
+ const plan = buildSwarmPlanFromTask({
9625
+ request: task,
9626
+ contract,
9627
+ index: indexInfo.index,
9628
+ });
9629
+ const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9630
+ const maxParallel = Math.max(1, Math.min(20, options.maxParallel ?? 3));
9631
+ const meta = this.buildSwarmRunMeta({
9632
+ runId,
9633
+ source: "plain",
9634
+ request: task,
9635
+ contract,
9636
+ repoScaleMode: indexInfo.scaleMode,
9637
+ semanticStatus,
9638
+ maxParallel,
9639
+ budgetUsd: options.budgetUsd,
9640
+ });
9641
+ await this.executeSwarmRun({
9642
+ runId,
9643
+ plan,
9644
+ meta,
9645
+ contract,
9646
+ budgetUsd: options.budgetUsd,
9647
+ projectIndex: indexInfo.index,
9648
+ enableIncrementalIndex: indexInfo.scaleMode !== "small",
9649
+ });
9650
+ }
9651
+ async runSwarmFromSingular(input) {
9652
+ const analysis = this.loadSingularAnalysisByRunId(input.runId);
9653
+ if (!analysis) {
9654
+ this.showWarning(`Singular run not found: ${input.runId}`);
9655
+ return;
9656
+ }
9657
+ const option = analysis.options.find((item) => item.id === String(input.option));
9658
+ if (!option) {
9659
+ this.showWarning(`Option ${input.option} not found in singular run ${input.runId}.`);
9660
+ return;
9661
+ }
9662
+ const contract = await this.ensureSwarmEffectiveContract(analysis.request);
9663
+ if (!contract)
9664
+ return;
9665
+ const indexInfo = this.ensureSwarmProjectIndex(`${analysis.request} ${option.title}`);
9666
+ const semanticStatus = await this.maybeWarnSwarmSemantic(indexInfo.scaleMode);
9667
+ const plan = buildSwarmPlanFromSingular({
9668
+ analysis,
9669
+ option,
9670
+ contract,
9671
+ index: indexInfo.index,
9672
+ });
9673
+ const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
9674
+ const maxParallel = Math.max(1, Math.min(20, input.maxParallel ?? 3));
9675
+ const meta = this.buildSwarmRunMeta({
9676
+ runId,
9677
+ source: "singular",
9678
+ request: analysis.request,
9679
+ contract,
9680
+ repoScaleMode: indexInfo.scaleMode,
9681
+ semanticStatus,
9682
+ maxParallel,
9683
+ budgetUsd: input.budgetUsd,
9684
+ linkedSingularRunId: analysis.runId,
9685
+ linkedSingularOption: option.id,
9686
+ });
9687
+ await this.executeSwarmRun({
9688
+ runId,
9689
+ plan,
9690
+ meta,
9691
+ contract,
9692
+ budgetUsd: input.budgetUsd,
9693
+ projectIndex: indexInfo.index,
9694
+ enableIncrementalIndex: indexInfo.scaleMode !== "small",
9695
+ });
9696
+ }
9697
+ loadSwarmRunBundle(runId) {
9698
+ const store = new SwarmStateStore(this.sessionManager.getCwd(), runId);
9699
+ const meta = store.loadMeta();
9700
+ const plan = store.loadPlan();
9701
+ const state = store.loadState();
9702
+ if (!meta || !plan || !state)
9703
+ return undefined;
9704
+ return { meta, plan, state };
9705
+ }
9706
+ computeSwarmCriticalPathLength(plan) {
9707
+ const byId = new Map(plan.tasks.map((task) => [task.id, task]));
9708
+ const memo = new Map();
9709
+ const visiting = new Set();
9710
+ const visit = (taskId) => {
9711
+ if (memo.has(taskId))
9712
+ return memo.get(taskId);
9713
+ if (visiting.has(taskId))
9714
+ return 1;
9715
+ visiting.add(taskId);
9716
+ const task = byId.get(taskId);
9717
+ if (!task)
9718
+ return 1;
9719
+ const depMax = task.depends_on.reduce((maxValue, depId) => Math.max(maxValue, visit(depId)), 0);
9720
+ const value = depMax + 1;
9721
+ memo.set(taskId, value);
9722
+ visiting.delete(taskId);
9723
+ return value;
9724
+ };
9725
+ let best = 0;
9726
+ for (const task of plan.tasks) {
9727
+ best = Math.max(best, visit(task.id));
9728
+ }
9729
+ return best;
9730
+ }
9731
+ computeSwarmDependentCounts(plan) {
9732
+ const counts = new Map();
9733
+ for (const task of plan.tasks) {
9734
+ for (const dep of task.depends_on) {
9735
+ counts.set(dep, (counts.get(dep) ?? 0) + 1);
9736
+ }
9737
+ }
9738
+ return counts;
9739
+ }
9740
+ formatSwarmWatch(meta, plan, state) {
9741
+ const tasks = Object.values(state.tasks);
9742
+ const done = tasks.filter((task) => task.status === "done").length;
9743
+ const running = tasks.filter((task) => task.status === "running").length;
9744
+ const blocked = tasks.filter((task) => task.status === "blocked").length;
9745
+ const errors = tasks.filter((task) => task.status === "error").length;
9746
+ const pending = tasks.filter((task) => task.status === "pending" || task.status === "ready").length;
9747
+ const total = tasks.length;
9748
+ const remaining = Math.max(0, total - done);
9749
+ const throughputPerTick = done > 0 && state.tick > 0 ? done / state.tick : 0;
9750
+ const etaTicks = throughputPerTick > 0 ? Math.ceil(remaining / throughputPerTick) : undefined;
9751
+ const criticalPath = this.computeSwarmCriticalPathLength(plan);
9752
+ const speedupPotential = criticalPath > 0 ? total / criticalPath : 1;
9753
+ const dependentCounts = this.computeSwarmDependentCounts(plan);
9754
+ const bottlenecks = [...dependentCounts.entries()]
9755
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
9756
+ .slice(0, 3);
9757
+ const lines = [
9758
+ `run_id: ${meta.runId}`,
9759
+ `status: ${state.status}`,
9760
+ `source: ${meta.source}`,
9761
+ `request: ${meta.request}`,
9762
+ "consistency_model: Scopes -> Touches -> Locks -> Gates -> Done",
9763
+ `progress: done=${done}/${tasks.length} running=${running} pending=${pending} blocked=${blocked} error=${errors}`,
9764
+ `budget_usd: ${state.budget.spentUsd.toFixed(2)}${meta.budgetUsd ? `/${meta.budgetUsd.toFixed(2)}` : ""} warned80=${state.budget.warned80 ? "yes" : "no"}`,
9765
+ `tick: ${state.tick} no_progress_ticks: ${state.noProgressTicks}`,
9766
+ `eta_ticks: ${etaTicks ?? "unknown"} throughput_per_tick=${throughputPerTick > 0 ? throughputPerTick.toFixed(2) : "0.00"}`,
9767
+ `critical_path: ${criticalPath} theoretical_speedup=${speedupPotential.toFixed(2)}x`,
9768
+ `repo_scale: ${meta.repoScaleMode} semantic: ${meta.semanticStatus ?? "unknown"}`,
9769
+ "",
9770
+ ...(bottlenecks.length > 0
9771
+ ? ["bottlenecks:", ...bottlenecks.map(([taskId, dependents]) => `- ${taskId}: unlocks ${dependents} downstream task(s)`), ""]
9772
+ : []),
9773
+ "tasks:",
9774
+ ...plan.tasks.map((task) => {
9775
+ const runtime = state.tasks[task.id];
9776
+ if (!runtime)
9777
+ return `- ${task.id}: missing runtime state`;
9778
+ return `- ${task.id}: ${runtime.status} attempts=${runtime.attempts} touches=${runtime.touches.slice(0, 3).join(",") || "-"}`;
9779
+ }),
9780
+ ];
9781
+ if (Object.keys(state.locks).length > 0) {
9782
+ lines.push("", "locks:");
9783
+ for (const [taskId, touches] of Object.entries(state.locks)) {
9784
+ lines.push(`- ${taskId}: ${touches.join(", ")}`);
9785
+ }
9786
+ }
9787
+ return lines.join("\n");
9788
+ }
9789
+ async runSwarmResume(runId) {
9790
+ const bundle = this.loadSwarmRunBundle(runId);
9791
+ if (!bundle) {
9792
+ this.showWarning(`Swarm run not found or incomplete: ${runId}`);
9793
+ return;
9794
+ }
9795
+ if (bundle.state.status === "completed") {
9796
+ this.showStatus(`Swarm run ${runId} is already completed.`);
9797
+ return;
9798
+ }
9799
+ bundle.state.status = "running";
9800
+ bundle.state.updatedAt = new Date().toISOString();
9801
+ await this.executeSwarmRun({
9802
+ runId,
9803
+ plan: bundle.plan,
9804
+ meta: bundle.meta,
9805
+ contract: bundle.meta.contract,
9806
+ budgetUsd: bundle.meta.budgetUsd,
9807
+ resumeState: bundle.state,
9808
+ projectIndex: bundle.meta.repoScaleMode === "small" ? undefined : loadProjectIndex(this.sessionManager.getCwd()),
9809
+ enableIncrementalIndex: bundle.meta.repoScaleMode !== "small",
9810
+ });
9811
+ }
9812
+ async runSwarmRetry(runId, taskId, resetBrief) {
9813
+ const bundle = this.loadSwarmRunBundle(runId);
9814
+ if (!bundle) {
9815
+ this.showWarning(`Swarm run not found or incomplete: ${runId}`);
9816
+ return;
9817
+ }
9818
+ const target = bundle.state.tasks[taskId];
9819
+ if (!target) {
9820
+ this.showWarning(`Task ${taskId} not found in run ${runId}.`);
9821
+ return;
9822
+ }
9823
+ if (resetBrief) {
9824
+ const existingPlan = bundle.plan.tasks.find((task) => task.id === taskId);
9825
+ const edited = await this.showExtensionInput(`/swarm retry: update brief for ${taskId}`, existingPlan?.brief ?? target.id);
9826
+ if (edited === undefined) {
9827
+ this.showStatus("Swarm retry cancelled.");
9828
+ return;
9829
+ }
9830
+ if (existingPlan) {
9831
+ existingPlan.brief = edited.trim() || existingPlan.brief;
9832
+ }
9833
+ }
9834
+ target.status = "ready";
9835
+ target.lastError = undefined;
9836
+ target.completedAt = undefined;
9837
+ bundle.state.retries[taskId] = 0;
9838
+ bundle.state.status = "running";
9839
+ bundle.state.updatedAt = new Date().toISOString();
9840
+ const store = new SwarmStateStore(this.sessionManager.getCwd(), runId);
9841
+ store.savePlan(bundle.plan);
9842
+ store.saveState(bundle.state);
9843
+ store.appendEvent({
9844
+ type: "task_retry",
9845
+ timestamp: new Date().toISOString(),
9846
+ runId,
9847
+ taskId,
9848
+ message: resetBrief ? "manual retry with reset brief" : "manual retry",
9849
+ });
9850
+ await this.executeSwarmRun({
9851
+ runId,
9852
+ plan: bundle.plan,
9853
+ meta: bundle.meta,
9854
+ contract: bundle.meta.contract,
9855
+ budgetUsd: bundle.meta.budgetUsd,
9856
+ resumeState: bundle.state,
9857
+ projectIndex: bundle.meta.repoScaleMode === "small" ? undefined : loadProjectIndex(this.sessionManager.getCwd()),
9858
+ enableIncrementalIndex: bundle.meta.repoScaleMode !== "small",
9859
+ });
9860
+ }
9861
+ async handleSwarmCommand(text) {
9862
+ if (this.session.isStreaming) {
9863
+ this.showWarning("Cannot run /swarm while the agent is processing another request.");
9864
+ return;
9865
+ }
9866
+ if (this.session.isCompacting) {
9867
+ this.showWarning("Cannot run /swarm while compaction is running.");
9868
+ return;
9869
+ }
9870
+ const parsed = this.parseSwarmCommand(text);
9871
+ if (!parsed)
9872
+ return;
9873
+ if (parsed.subcommand === "help") {
9874
+ this.showCommandTextBlock("Swarm Help", this.getSwarmHelpText());
9875
+ return;
9876
+ }
9877
+ if (parsed.subcommand === "watch") {
9878
+ let runId = parsed.runId;
9879
+ if (!runId) {
9880
+ const runs = SwarmStateStore.listRuns(this.sessionManager.getCwd(), 20);
9881
+ if (runs.length === 0) {
9882
+ this.showStatus("No swarm runs found.");
9883
+ return;
9884
+ }
9885
+ runId = runs[0].runId;
9886
+ }
9887
+ const bundle = this.loadSwarmRunBundle(runId);
9888
+ if (!bundle) {
9889
+ this.showWarning(`Swarm run not found or incomplete: ${runId}`);
9890
+ return;
9891
+ }
9892
+ this.showCommandTextBlock("Swarm Watch", this.formatSwarmWatch(bundle.meta, bundle.plan, bundle.state));
9893
+ return;
9894
+ }
9895
+ if (parsed.subcommand === "resume") {
9896
+ await this.runSwarmResume(parsed.runId);
9897
+ return;
9898
+ }
9899
+ if (parsed.subcommand === "retry") {
9900
+ await this.runSwarmRetry(parsed.runId, parsed.taskId, parsed.resetBrief);
9901
+ return;
9902
+ }
9903
+ if (parsed.subcommand === "run") {
9904
+ await this.runSwarmFromTask(parsed.task, {
9905
+ maxParallel: parsed.maxParallel,
9906
+ budgetUsd: parsed.budgetUsd,
9907
+ });
9908
+ return;
9909
+ }
9910
+ if (parsed.subcommand === "from-singular") {
9911
+ await this.runSwarmFromSingular({
9912
+ runId: parsed.runId,
9913
+ option: parsed.option,
9914
+ maxParallel: parsed.maxParallel,
9915
+ budgetUsd: parsed.budgetUsd,
9916
+ });
9917
+ }
9918
+ }
8388
9919
  parseOrchestrateSlashCommand(text) {
8389
9920
  const args = this.parseSlashArgs(text).slice(1);
8390
9921
  let mode;
@@ -8590,10 +10121,20 @@ export class InteractiveMode {
8590
10121
  this.showWarning("Cannot run /orchestrate while compaction is running.");
8591
10122
  return;
8592
10123
  }
10124
+ const rawArgs = this.parseSlashArgs(text).slice(1);
10125
+ if (rawArgs.includes("--swarm")) {
10126
+ this.showWarning("`/orchestrate --swarm` was removed to avoid ambiguity. Use `/swarm` commands directly.");
10127
+ this.showCommandTextBlock("Swarm Usage", this.getSwarmHelpText());
10128
+ return;
10129
+ }
8593
10130
  const parsed = this.parseOrchestrateSlashCommand(text);
8594
10131
  if (!parsed) {
8595
10132
  return;
8596
10133
  }
10134
+ const swarmRecommendation = this.buildSwarmRecommendationFromOrchestrate(parsed);
10135
+ if (swarmRecommendation.recommend) {
10136
+ this.showWarning(`This task looks complex/risky for legacy /orchestrate (${swarmRecommendation.reasons.join("; ")}). Consider ${swarmRecommendation.command}.`);
10137
+ }
8597
10138
  const currentCwd = this.sessionManager.getCwd();
8598
10139
  const assignments = [];
8599
10140
  const assignmentRecords = [];
@@ -8637,6 +10178,7 @@ export class InteractiveMode {
8637
10178
  "- use task tool for every agent assignment",
8638
10179
  "- for parallel mode, emit all independent task calls in one assistant response",
8639
10180
  "- in parallel mode, use parallel tool-call style (<use_parallel_tool_calls>)",
10181
+ "- when assignment lines include depends_on, still emit one task call per assignment; runtime enforces dependency gating",
8640
10182
  "- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
8641
10183
  "- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
8642
10184
  "- include run_id and task_id from each assignment in the task tool arguments",
@@ -10746,6 +12288,7 @@ The agent will automatically receive IOSM context on every turn.`;
10746
12288
  return result;
10747
12289
  }
10748
12290
  stop() {
12291
+ this.clearSubagentElapsedTimer();
10749
12292
  if (this.loadingAnimation) {
10750
12293
  this.loadingAnimation.stop();
10751
12294
  this.loadingAnimation = undefined;