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.
- package/CHANGELOG.md +53 -0
- package/README.md +64 -52
- package/dist/core/agent-teams.d.ts.map +1 -1
- package/dist/core/agent-teams.js +38 -11
- package/dist/core/agent-teams.js.map +1 -1
- package/dist/core/failure-retrospective.d.ts +12 -0
- package/dist/core/failure-retrospective.d.ts.map +1 -0
- package/dist/core/failure-retrospective.js +115 -0
- package/dist/core/failure-retrospective.js.map +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -3
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/models-dev-provider-catalog.d.ts +30 -0
- package/dist/core/models-dev-provider-catalog.d.ts.map +1 -0
- package/dist/core/models-dev-provider-catalog.js +118 -0
- package/dist/core/models-dev-provider-catalog.js.map +1 -0
- package/dist/core/models-dev-providers.d.ts +12 -0
- package/dist/core/models-dev-providers.d.ts.map +1 -0
- package/dist/core/models-dev-providers.js +736 -0
- package/dist/core/models-dev-providers.js.map +1 -0
- package/dist/core/project-index/index.d.ts +17 -0
- package/dist/core/project-index/index.d.ts.map +1 -0
- package/dist/core/project-index/index.js +323 -0
- package/dist/core/project-index/index.js.map +1 -0
- package/dist/core/project-index/types.d.ts +34 -0
- package/dist/core/project-index/types.d.ts.map +1 -0
- package/dist/core/project-index/types.js +2 -0
- package/dist/core/project-index/types.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +8 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/shared-memory.d.ts +46 -0
- package/dist/core/shared-memory.d.ts.map +1 -0
- package/dist/core/shared-memory.js +253 -0
- package/dist/core/shared-memory.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagents.js +1 -1
- package/dist/core/subagents.js.map +1 -1
- package/dist/core/swarm/gates.d.ts +9 -0
- package/dist/core/swarm/gates.d.ts.map +1 -0
- package/dist/core/swarm/gates.js +65 -0
- package/dist/core/swarm/gates.js.map +1 -0
- package/dist/core/swarm/index.d.ts +9 -0
- package/dist/core/swarm/index.d.ts.map +1 -0
- package/dist/core/swarm/index.js +9 -0
- package/dist/core/swarm/index.js.map +1 -0
- package/dist/core/swarm/locks.d.ts +21 -0
- package/dist/core/swarm/locks.d.ts.map +1 -0
- package/dist/core/swarm/locks.js +93 -0
- package/dist/core/swarm/locks.js.map +1 -0
- package/dist/core/swarm/planner.d.ts +16 -0
- package/dist/core/swarm/planner.d.ts.map +1 -0
- package/dist/core/swarm/planner.js +137 -0
- package/dist/core/swarm/planner.js.map +1 -0
- package/dist/core/swarm/retry.d.ts +16 -0
- package/dist/core/swarm/retry.d.ts.map +1 -0
- package/dist/core/swarm/retry.js +32 -0
- package/dist/core/swarm/retry.js.map +1 -0
- package/dist/core/swarm/scheduler.d.ts +48 -0
- package/dist/core/swarm/scheduler.d.ts.map +1 -0
- package/dist/core/swarm/scheduler.js +554 -0
- package/dist/core/swarm/scheduler.js.map +1 -0
- package/dist/core/swarm/spawn.d.ts +16 -0
- package/dist/core/swarm/spawn.d.ts.map +1 -0
- package/dist/core/swarm/spawn.js +42 -0
- package/dist/core/swarm/spawn.js.map +1 -0
- package/dist/core/swarm/state-store.d.ts +35 -0
- package/dist/core/swarm/state-store.d.ts.map +1 -0
- package/dist/core/swarm/state-store.js +106 -0
- package/dist/core/swarm/state-store.js.map +1 -0
- package/dist/core/swarm/types.d.ts +116 -0
- package/dist/core/swarm/types.d.ts.map +1 -0
- package/dist/core/swarm/types.js +2 -0
- package/dist/core/swarm/types.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/shared-memory.d.ts +23 -0
- package/dist/core/tools/shared-memory.d.ts.map +1 -0
- package/dist/core/tools/shared-memory.js +134 -0
- package/dist/core/tools/shared-memory.js.map +1 -0
- package/dist/core/tools/task.d.ts +8 -1
- package/dist/core/tools/task.d.ts.map +1 -1
- package/dist/core/tools/task.js +664 -123
- package/dist/core/tools/task.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +3 -11
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +1 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +27 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +13 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +89 -27
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/subagent-message.js +14 -0
- package/dist/modes/interactive/components/subagent-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +50 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1594 -51
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +11 -1
- package/docs/configuration.md +4 -1
- package/docs/getting-started.md +2 -2
- package/docs/interactive-mode.md +43 -4
- package/docs/orchestration-and-subagents.md +96 -169
- 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
|
-
|
|
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.
|
|
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 (
|
|
5398
|
+
const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (provider) => {
|
|
5273
5399
|
done();
|
|
5274
5400
|
if (mode === "login") {
|
|
5275
|
-
if (
|
|
5276
|
-
|
|
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(
|
|
5410
|
+
await this.showLoginDialog(provider.id);
|
|
5280
5411
|
}
|
|
5281
5412
|
}
|
|
5282
5413
|
else {
|
|
5283
5414
|
// Logout flow
|
|
5284
|
-
const providerName = this.getProviderDisplayName(
|
|
5415
|
+
const providerName = this.getProviderDisplayName(provider.id);
|
|
5285
5416
|
try {
|
|
5286
|
-
this.session.modelRegistry.authStorage.logout(
|
|
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
|
-
|
|
5304
|
-
|
|
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
|
|
5575
|
+
async handleApiKeyLogin(providerId, options) {
|
|
5576
|
+
const providerName = options?.providerName || this.getProviderDisplayName(providerId);
|
|
5310
5577
|
const openModelSelector = options?.openModelSelector ?? true;
|
|
5311
|
-
const
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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: ${
|
|
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;
|