iosm-cli 0.2.0 → 0.2.1
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/README.md +58 -47
- 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/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/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/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 +38 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1362 -31
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +11 -1
- package/docs/interactive-mode.md +42 -3
- package/docs/orchestration-and-subagents.md +96 -169
- package/package.json +4 -3
|
@@ -20,11 +20,13 @@ import { resolveModelScope } from "../../core/model-resolver.js";
|
|
|
20
20
|
import { getMcpCommandHelp, parseMcpAddCommand, parseMcpTargetCommand, } from "../../core/mcp/index.js";
|
|
21
21
|
import { addMemoryEntry, getMemoryFilePath, readMemoryEntries, removeMemoryEntry, updateMemoryEntry, } from "../../core/memory.js";
|
|
22
22
|
import { ContractService, normalizeEngineeringContract, } from "../../core/contract.js";
|
|
23
|
+
import { buildProjectIndex, collectChangedFilesSince, ensureProjectIndex, loadProjectIndex, queryProjectIndex, saveProjectIndex, } from "../../core/project-index/index.js";
|
|
23
24
|
import { SingularService, } from "../../core/singular.js";
|
|
24
25
|
import { getDefaultSemanticSearchConfig, getSemanticConfigPath, getSemanticIndexDir, isLikelyEmbeddingModelId, listOllamaLocalModels, listOpenRouterEmbeddingModels, loadMergedSemanticConfig, readScopedSemanticConfig, SemanticConfigMissingError, SemanticIndexRequiredError, SemanticRebuildRequiredError, SemanticSearchRuntime, upsertScopedSemanticSearchConfig, } from "../../core/semantic/index.js";
|
|
25
26
|
import { DefaultResourceLoader } from "../../core/resource-loader.js";
|
|
26
27
|
import { createAgentSession } from "../../core/sdk.js";
|
|
27
28
|
import { createTeamRun, getTeamRun, listTeamRuns } from "../../core/agent-teams.js";
|
|
29
|
+
import { buildSwarmPlanFromSingular, buildSwarmPlanFromTask, runSwarmScheduler, SwarmStateStore, } from "../../core/swarm/index.js";
|
|
28
30
|
import { loadCustomSubagents, resolveCustomSubagentReference, } from "../../core/subagents.js";
|
|
29
31
|
import { getSubagentRun, listSubagentRuns } from "../../core/subagent-runs.js";
|
|
30
32
|
import { SessionManager } from "../../core/session-manager.js";
|
|
@@ -362,6 +364,7 @@ export class InteractiveMode {
|
|
|
362
364
|
this.permissionPromptLock = Promise.resolve();
|
|
363
365
|
this.sessionAllowedToolSignatures = new Set();
|
|
364
366
|
this.singularLastEffectiveContract = {};
|
|
367
|
+
this.swarmActiveRunId = undefined;
|
|
365
368
|
this.lastSigintTime = 0;
|
|
366
369
|
this.lastEscapeTime = 0;
|
|
367
370
|
this.changelogMarkdown = undefined;
|
|
@@ -375,6 +378,7 @@ export class InteractiveMode {
|
|
|
375
378
|
this.pendingTools = new Map();
|
|
376
379
|
// Subagent execution tracking with live progress/metadata for task tool calls.
|
|
377
380
|
this.subagentComponents = new Map();
|
|
381
|
+
this.subagentElapsedTimer = undefined;
|
|
378
382
|
// Internal UI metadata emitted by runtime for orchestration rendering.
|
|
379
383
|
this.pendingInternalUserDisplayAliases = [];
|
|
380
384
|
this.pendingAssistantOrchestrationContexts = 0;
|
|
@@ -1010,6 +1014,32 @@ export class InteractiveMode {
|
|
|
1010
1014
|
}
|
|
1011
1015
|
return null;
|
|
1012
1016
|
}
|
|
1017
|
+
getSwarmArgumentCompletions(prefix) {
|
|
1018
|
+
const subcommands = ["run", "from-singular", "watch", "retry", "resume", "help"];
|
|
1019
|
+
const hasTrailingSpace = /\\s$/.test(prefix);
|
|
1020
|
+
const tokens = this.parseSlashArgs(prefix);
|
|
1021
|
+
const first = tokens[0]?.toLowerCase();
|
|
1022
|
+
if (!first || (tokens.length === 1 && !hasTrailingSpace)) {
|
|
1023
|
+
const query = first ?? "";
|
|
1024
|
+
return subcommands.filter((item) => item.includes(query)).map((item) => ({ value: item, label: item }));
|
|
1025
|
+
}
|
|
1026
|
+
const active = first;
|
|
1027
|
+
if (active === "run" || active === "from-singular") {
|
|
1028
|
+
const flags = ["--max-parallel", "--budget-usd", ...(active === "from-singular" ? ["--option"] : [])];
|
|
1029
|
+
const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
|
|
1030
|
+
if (!query || query.startsWith("--")) {
|
|
1031
|
+
return flags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (active === "retry") {
|
|
1035
|
+
const flags = ["--reset-brief"];
|
|
1036
|
+
const query = hasTrailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
|
|
1037
|
+
if (!query || query.startsWith("--")) {
|
|
1038
|
+
return flags.filter((flag) => flag.includes(query)).map((flag) => ({ value: flag, label: flag }));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1013
1043
|
setupAutocomplete(fdPath) {
|
|
1014
1044
|
// Define commands for autocomplete
|
|
1015
1045
|
const builtinCommands = BUILTIN_SLASH_COMMANDS.filter((command) => this.activeProfileName === "iosm" || !IOSM_PROFILE_ONLY_COMMANDS.has(command.name));
|
|
@@ -1063,6 +1093,10 @@ export class InteractiveMode {
|
|
|
1063
1093
|
if (singularCommand) {
|
|
1064
1094
|
singularCommand.getArgumentCompletions = (prefix) => this.getSingularArgumentCompletions(prefix);
|
|
1065
1095
|
}
|
|
1096
|
+
const swarmCommand = slashCommands.find((command) => command.name === "swarm");
|
|
1097
|
+
if (swarmCommand) {
|
|
1098
|
+
swarmCommand.getArgumentCompletions = (prefix) => this.getSwarmArgumentCompletions(prefix);
|
|
1099
|
+
}
|
|
1066
1100
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
1067
1101
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
1068
1102
|
name: cmd.name,
|
|
@@ -2581,6 +2615,11 @@ export class InteractiveMode {
|
|
|
2581
2615
|
await this.handleOrchestrateSlashCommand(text);
|
|
2582
2616
|
return;
|
|
2583
2617
|
}
|
|
2618
|
+
if (text === "/swarm" || text.startsWith("/swarm ")) {
|
|
2619
|
+
this.editor.setText("");
|
|
2620
|
+
await this.handleSwarmCommand(text);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2584
2623
|
if (text === "/agents" || text.startsWith("/agents ")) {
|
|
2585
2624
|
this.editor.setText("");
|
|
2586
2625
|
await this.handleAgentsSlashCommand(text);
|
|
@@ -2839,6 +2878,59 @@ export class InteractiveMode {
|
|
|
2839
2878
|
this.editor.addToHistory?.(text);
|
|
2840
2879
|
};
|
|
2841
2880
|
}
|
|
2881
|
+
updateRunningSubagentDisplay(subagent) {
|
|
2882
|
+
subagent.component.update({
|
|
2883
|
+
description: subagent.description,
|
|
2884
|
+
profile: subagent.profile,
|
|
2885
|
+
status: "running",
|
|
2886
|
+
phase: subagent.phase ?? "running",
|
|
2887
|
+
phaseState: subagent.phaseState,
|
|
2888
|
+
cwd: subagent.cwd,
|
|
2889
|
+
agent: subagent.agent,
|
|
2890
|
+
lockKey: subagent.lockKey,
|
|
2891
|
+
isolation: subagent.isolation,
|
|
2892
|
+
activeTool: subagent.activeTool,
|
|
2893
|
+
toolCallsStarted: subagent.toolCallsStarted,
|
|
2894
|
+
toolCallsCompleted: subagent.toolCallsCompleted,
|
|
2895
|
+
assistantMessages: subagent.assistantMessages,
|
|
2896
|
+
delegatedTasks: subagent.delegatedTasks,
|
|
2897
|
+
delegatedSucceeded: subagent.delegatedSucceeded,
|
|
2898
|
+
delegatedFailed: subagent.delegatedFailed,
|
|
2899
|
+
delegateIndex: subagent.delegateIndex,
|
|
2900
|
+
delegateTotal: subagent.delegateTotal,
|
|
2901
|
+
delegateDescription: subagent.delegateDescription,
|
|
2902
|
+
delegateProfile: subagent.delegateProfile,
|
|
2903
|
+
delegateItems: subagent.delegateItems,
|
|
2904
|
+
durationMs: Date.now() - subagent.startTime,
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
ensureSubagentElapsedTimer() {
|
|
2908
|
+
if (this.subagentElapsedTimer || this.subagentComponents.size === 0) {
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
this.subagentElapsedTimer = setInterval(() => {
|
|
2912
|
+
if (this.subagentComponents.size === 0) {
|
|
2913
|
+
this.clearSubagentElapsedTimer();
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
for (const subagent of this.subagentComponents.values()) {
|
|
2917
|
+
this.updateRunningSubagentDisplay(subagent);
|
|
2918
|
+
}
|
|
2919
|
+
this.ui.requestRender();
|
|
2920
|
+
}, 1000);
|
|
2921
|
+
}
|
|
2922
|
+
clearSubagentElapsedTimer() {
|
|
2923
|
+
if (!this.subagentElapsedTimer) {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
clearInterval(this.subagentElapsedTimer);
|
|
2927
|
+
this.subagentElapsedTimer = undefined;
|
|
2928
|
+
}
|
|
2929
|
+
stopSubagentElapsedTimerIfIdle() {
|
|
2930
|
+
if (this.subagentComponents.size === 0) {
|
|
2931
|
+
this.clearSubagentElapsedTimer();
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2842
2934
|
subscribeToAgent() {
|
|
2843
2935
|
this.unsubscribe = this.session.subscribe(async (event) => {
|
|
2844
2936
|
await this.handleEvent(event);
|
|
@@ -3020,6 +3112,7 @@ export class InteractiveMode {
|
|
|
3020
3112
|
delegateProfile: info.delegateProfile,
|
|
3021
3113
|
delegateItems: info.delegateItems,
|
|
3022
3114
|
});
|
|
3115
|
+
this.ensureSubagentElapsedTimer();
|
|
3023
3116
|
this.ui.requestRender();
|
|
3024
3117
|
}
|
|
3025
3118
|
else if (event.toolName !== "task" && !this.pendingTools.has(event.toolCallId)) {
|
|
@@ -3105,29 +3198,7 @@ export class InteractiveMode {
|
|
|
3105
3198
|
subagent.phase = text.trim();
|
|
3106
3199
|
}
|
|
3107
3200
|
}
|
|
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
|
-
});
|
|
3201
|
+
this.updateRunningSubagentDisplay(subagent);
|
|
3131
3202
|
this.ui.requestRender();
|
|
3132
3203
|
break;
|
|
3133
3204
|
}
|
|
@@ -3191,6 +3262,7 @@ export class InteractiveMode {
|
|
|
3191
3262
|
this.pendingTools.delete(event.toolCallId);
|
|
3192
3263
|
}
|
|
3193
3264
|
this.subagentComponents.delete(event.toolCallId);
|
|
3265
|
+
this.stopSubagentElapsedTimerIfIdle();
|
|
3194
3266
|
this.ui.requestRender();
|
|
3195
3267
|
break;
|
|
3196
3268
|
}
|
|
@@ -3215,6 +3287,7 @@ export class InteractiveMode {
|
|
|
3215
3287
|
}
|
|
3216
3288
|
this.pendingTools.clear();
|
|
3217
3289
|
this.subagentComponents.clear();
|
|
3290
|
+
this.clearSubagentElapsedTimer();
|
|
3218
3291
|
await this.checkShutdownRequested();
|
|
3219
3292
|
this.ui.requestRender();
|
|
3220
3293
|
break;
|
|
@@ -6838,7 +6911,105 @@ export class InteractiveMode {
|
|
|
6838
6911
|
}
|
|
6839
6912
|
return lines.length > 0 ? lines.join("\n") : "- none";
|
|
6840
6913
|
}
|
|
6841
|
-
|
|
6914
|
+
resolveSingularRepoScaleMode(baseline) {
|
|
6915
|
+
if (baseline.scannedFiles >= 8000 || baseline.sourceFiles >= 4000) {
|
|
6916
|
+
return {
|
|
6917
|
+
mode: "large",
|
|
6918
|
+
reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
|
|
6919
|
+
};
|
|
6920
|
+
}
|
|
6921
|
+
if (baseline.scannedFiles >= 2500 || baseline.sourceFiles >= 1200) {
|
|
6922
|
+
return {
|
|
6923
|
+
mode: "medium",
|
|
6924
|
+
reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
|
|
6925
|
+
};
|
|
6926
|
+
}
|
|
6927
|
+
return {
|
|
6928
|
+
mode: "small",
|
|
6929
|
+
reason: `scanned=${baseline.scannedFiles}, source=${baseline.sourceFiles}`,
|
|
6930
|
+
};
|
|
6931
|
+
}
|
|
6932
|
+
buildSingularSemanticGuidanceFromStatus(status) {
|
|
6933
|
+
if (!status.configured) {
|
|
6934
|
+
return {
|
|
6935
|
+
statusLine: "not_configured",
|
|
6936
|
+
promptGuidance: [
|
|
6937
|
+
"Semantic index is unavailable; use narrow path-based discovery and avoid wide repo scans.",
|
|
6938
|
+
"If confidence is low, explicitly ask user to run /semantic setup and /semantic index, then rerun /singular.",
|
|
6939
|
+
],
|
|
6940
|
+
operatorHint: "Large/medium repo mode: semantic index is not configured. Run /semantic setup, then /semantic index.",
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
if (!status.enabled) {
|
|
6944
|
+
return {
|
|
6945
|
+
statusLine: "configured_but_disabled",
|
|
6946
|
+
promptGuidance: [
|
|
6947
|
+
"Semantic index is configured but disabled; proceed with targeted rg/read steps only.",
|
|
6948
|
+
"If discovery quality is insufficient, ask user to enable semantic search in /semantic setup.",
|
|
6949
|
+
],
|
|
6950
|
+
operatorHint: "Large/medium repo mode: semantic index is disabled. Enable it in /semantic setup for faster planning.",
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6953
|
+
if (!status.indexed) {
|
|
6954
|
+
return {
|
|
6955
|
+
statusLine: "configured_not_indexed",
|
|
6956
|
+
promptGuidance: [
|
|
6957
|
+
"Semantic index is configured but missing; do focused discovery and avoid broad scans.",
|
|
6958
|
+
"If context coverage is insufficient, ask user to run /semantic index before final recommendation.",
|
|
6959
|
+
],
|
|
6960
|
+
operatorHint: "Large/medium repo mode: semantic index is missing. Run /semantic index.",
|
|
6961
|
+
};
|
|
6962
|
+
}
|
|
6963
|
+
if (status.stale) {
|
|
6964
|
+
const requiresRebuild = status.staleReason === "provider_changed" ||
|
|
6965
|
+
status.staleReason === "chunking_changed" ||
|
|
6966
|
+
status.staleReason === "index_filters_changed" ||
|
|
6967
|
+
status.staleReason === "dimension_mismatch";
|
|
6968
|
+
return {
|
|
6969
|
+
statusLine: `stale${status.staleReason ? ` (${status.staleReason})` : ""}`,
|
|
6970
|
+
promptGuidance: [
|
|
6971
|
+
"Semantic index is stale; treat semantic hits as hints and verify with direct file reads.",
|
|
6972
|
+
"If index staleness blocks confidence, ask user to run /semantic rebuild or /semantic index.",
|
|
6973
|
+
],
|
|
6974
|
+
operatorHint: requiresRebuild
|
|
6975
|
+
? "Large/medium repo mode: semantic index is stale and requires /semantic rebuild."
|
|
6976
|
+
: "Large/medium repo mode: semantic index is stale. Run /semantic index.",
|
|
6977
|
+
};
|
|
6978
|
+
}
|
|
6979
|
+
return {
|
|
6980
|
+
statusLine: `ready (${status.provider}/${status.model}, files=${status.files}, chunks=${status.chunks}, auto_index=${status.autoIndex ? "on" : "off"})`,
|
|
6981
|
+
promptGuidance: [
|
|
6982
|
+
"Use semantic_search for first-pass discovery, then confirm with targeted reads and grep.",
|
|
6983
|
+
"Avoid full-tree scans unless evidence is still insufficient.",
|
|
6984
|
+
],
|
|
6985
|
+
};
|
|
6986
|
+
}
|
|
6987
|
+
async buildSingularSemanticGuidance(scaleMode) {
|
|
6988
|
+
if (scaleMode === "small") {
|
|
6989
|
+
return {
|
|
6990
|
+
statusLine: "optional_for_small_repo",
|
|
6991
|
+
promptGuidance: [
|
|
6992
|
+
"Prefer direct targeted reads/grep; semantic index is optional for this repository size.",
|
|
6993
|
+
],
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
try {
|
|
6997
|
+
const status = await this.createSemanticRuntime().status();
|
|
6998
|
+
return this.buildSingularSemanticGuidanceFromStatus(status);
|
|
6999
|
+
}
|
|
7000
|
+
catch (error) {
|
|
7001
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7002
|
+
return {
|
|
7003
|
+
statusLine: `status_unavailable (${message})`,
|
|
7004
|
+
promptGuidance: [
|
|
7005
|
+
"Semantic status is unavailable; proceed with conservative targeted discovery.",
|
|
7006
|
+
"If discovery quality is insufficient, ask user to configure /semantic setup and rerun /singular.",
|
|
7007
|
+
],
|
|
7008
|
+
operatorHint: `Large/medium repo mode: cannot read semantic status (${message}). Run /semantic status.`,
|
|
7009
|
+
};
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance) {
|
|
6842
7013
|
const filesHint = baseline.matchedFiles.length > 0
|
|
6843
7014
|
? baseline.matchedFiles.slice(0, 12).map((item) => `- ${item}`).join("\n")
|
|
6844
7015
|
: "- no direct file matches found in heuristic pass";
|
|
@@ -6895,6 +7066,12 @@ export class InteractiveMode {
|
|
|
6895
7066
|
`- baseline_blast_radius: ${baseline.baselineBlastRadius}`,
|
|
6896
7067
|
`- baseline_recommendation: ${baseline.recommendation}`,
|
|
6897
7068
|
"",
|
|
7069
|
+
"Repository runtime guidance:",
|
|
7070
|
+
`- scale_mode: ${runtimeGuidance.scaleMode}`,
|
|
7071
|
+
`- scale_reason: ${runtimeGuidance.scaleReason}`,
|
|
7072
|
+
`- semantic_status: ${runtimeGuidance.semanticStatusLine}`,
|
|
7073
|
+
...runtimeGuidance.semanticGuidance.map((line) => `- ${line}`),
|
|
7074
|
+
"",
|
|
6898
7075
|
"Matched file hints from baseline (verify, do not assume blindly):",
|
|
6899
7076
|
filesHint,
|
|
6900
7077
|
"",
|
|
@@ -7025,7 +7202,7 @@ export class InteractiveMode {
|
|
|
7025
7202
|
hasRecommendation: recommendationRaw !== undefined,
|
|
7026
7203
|
};
|
|
7027
7204
|
}
|
|
7028
|
-
async runSingularAgentFeasibilityPass(request, baseline, contract) {
|
|
7205
|
+
async runSingularAgentFeasibilityPass(request, baseline, contract, runtimeGuidance) {
|
|
7029
7206
|
const model = this.session.model;
|
|
7030
7207
|
if (!model) {
|
|
7031
7208
|
return undefined;
|
|
@@ -7076,7 +7253,7 @@ export class InteractiveMode {
|
|
|
7076
7253
|
});
|
|
7077
7254
|
this.singularAnalysisSession = session;
|
|
7078
7255
|
try {
|
|
7079
|
-
const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract);
|
|
7256
|
+
const primaryPrompt = this.buildSingularAgentPrompt(request, baseline, contract, runtimeGuidance);
|
|
7080
7257
|
const strictRetryPrompt = [
|
|
7081
7258
|
"Retry strict mode:",
|
|
7082
7259
|
"- Inspect repository files using tools first.",
|
|
@@ -7176,7 +7353,7 @@ export class InteractiveMode {
|
|
|
7176
7353
|
}
|
|
7177
7354
|
lines.push("");
|
|
7178
7355
|
lines.push("next_action:");
|
|
7179
|
-
lines.push(" choose option 1/2/3
|
|
7356
|
+
lines.push(" choose option 1/2/3, then pick Start with Swarm or Continue without Swarm");
|
|
7180
7357
|
return lines.join("\n");
|
|
7181
7358
|
}
|
|
7182
7359
|
buildSingularExecutionDraft(result, option, contract) {
|
|
@@ -7280,10 +7457,22 @@ export class InteractiveMode {
|
|
|
7280
7457
|
"cons:",
|
|
7281
7458
|
...picked.cons.map((item) => `- ${item}`),
|
|
7282
7459
|
].join("\n"));
|
|
7283
|
-
if (picked.id === "3"
|
|
7460
|
+
if (picked.id === "3") {
|
|
7284
7461
|
this.showStatus("Singular: defer option selected, implementation postponed.");
|
|
7285
7462
|
return;
|
|
7286
7463
|
}
|
|
7464
|
+
const executionChoice = await this.showExtensionSelector("/singular: execution mode", ["Start with Swarm (Recommended)", "Continue without Swarm", "Cancel"]);
|
|
7465
|
+
if (!executionChoice || executionChoice === "Cancel") {
|
|
7466
|
+
this.showStatus("Singular: execution cancelled.");
|
|
7467
|
+
return;
|
|
7468
|
+
}
|
|
7469
|
+
if (executionChoice.startsWith("Start with Swarm")) {
|
|
7470
|
+
await this.runSwarmFromSingular({
|
|
7471
|
+
runId: result.runId,
|
|
7472
|
+
option: Number.parseInt(picked.id, 10),
|
|
7473
|
+
});
|
|
7474
|
+
return;
|
|
7475
|
+
}
|
|
7287
7476
|
this.editor.setText(this.buildSingularExecutionDraft(result, picked, this.singularLastEffectiveContract));
|
|
7288
7477
|
this.showStatus("Singular: detailed execution draft generated in editor.");
|
|
7289
7478
|
}
|
|
@@ -7319,13 +7508,27 @@ export class InteractiveMode {
|
|
|
7319
7508
|
autosave: false,
|
|
7320
7509
|
contract: effectiveContract,
|
|
7321
7510
|
});
|
|
7511
|
+
const scale = this.resolveSingularRepoScaleMode(baseline);
|
|
7512
|
+
const semanticGuidance = await this.buildSingularSemanticGuidance(scale.mode);
|
|
7513
|
+
if (scale.mode !== "small") {
|
|
7514
|
+
this.showStatus(`Singular scale mode: ${scale.mode} (${scale.reason})`);
|
|
7515
|
+
}
|
|
7516
|
+
if (semanticGuidance.operatorHint) {
|
|
7517
|
+
this.showWarning(semanticGuidance.operatorHint);
|
|
7518
|
+
}
|
|
7519
|
+
const runtimeGuidance = {
|
|
7520
|
+
scaleMode: scale.mode,
|
|
7521
|
+
scaleReason: scale.reason,
|
|
7522
|
+
semanticStatusLine: semanticGuidance.statusLine,
|
|
7523
|
+
semanticGuidance: semanticGuidance.promptGuidance,
|
|
7524
|
+
};
|
|
7322
7525
|
let result = baseline;
|
|
7323
7526
|
if (!this.session.model) {
|
|
7324
7527
|
this.showWarning("No model selected. /singular used heuristic analysis only. Use /model to enable agent feasibility pass.");
|
|
7325
7528
|
}
|
|
7326
7529
|
else {
|
|
7327
7530
|
try {
|
|
7328
|
-
const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract);
|
|
7531
|
+
const enriched = await this.runSingularAgentFeasibilityPass(request, baseline, effectiveContract, runtimeGuidance);
|
|
7329
7532
|
if (enriched) {
|
|
7330
7533
|
result = enriched;
|
|
7331
7534
|
}
|
|
@@ -7362,6 +7565,9 @@ export class InteractiveMode {
|
|
|
7362
7565
|
" /singular last",
|
|
7363
7566
|
" /singular help",
|
|
7364
7567
|
"",
|
|
7568
|
+
"Flow:",
|
|
7569
|
+
" /singular -> choose option -> Start with Swarm or Continue without Swarm",
|
|
7570
|
+
"",
|
|
7365
7571
|
"Examples:",
|
|
7366
7572
|
" /singular add account dashboard",
|
|
7367
7573
|
" /singular introduce RBAC for API",
|
|
@@ -8206,13 +8412,28 @@ export class InteractiveMode {
|
|
|
8206
8412
|
return;
|
|
8207
8413
|
}
|
|
8208
8414
|
}
|
|
8415
|
+
const mentionTask = cleaned.length > 0 ? cleaned : userInput;
|
|
8416
|
+
const orchestrationAwareAgent = /orchestrator/i.test(mentionedAgent);
|
|
8417
|
+
const mentionMode = orchestrationAwareAgent ? "parallel" : "sequential";
|
|
8418
|
+
const mentionMaxParallel = orchestrationAwareAgent ? 20 : undefined;
|
|
8209
8419
|
const mentionPrompt = [
|
|
8210
|
-
|
|
8420
|
+
`<orchestrate mode="${mentionMode}" agents="1"${mentionMaxParallel ? ` max_parallel="${mentionMaxParallel}"` : ""}>`,
|
|
8211
8421
|
`- agent 1: profile=${this.activeProfileName} cwd=${this.sessionManager.getCwd()} agent=${mentionedAgent}`,
|
|
8212
|
-
`task: ${
|
|
8422
|
+
`task: ${mentionTask}`,
|
|
8213
8423
|
"constraints:",
|
|
8214
8424
|
"- user selected a concrete custom agent via @mention",
|
|
8215
8425
|
`- MUST call task tool with agent="${mentionedAgent}"`,
|
|
8426
|
+
...(orchestrationAwareAgent
|
|
8427
|
+
? [
|
|
8428
|
+
"- Include delegate_parallel_hint in the task call.",
|
|
8429
|
+
"- If user explicitly requested an agent count, set delegate_parallel_hint to that count (clamp 1..10).",
|
|
8430
|
+
"- Otherwise set delegate_parallel_hint based on complexity: simple=1, medium=3-6, complex/risky=7-10.",
|
|
8431
|
+
"- For non-trivial tasks, prefer delegate_parallel_hint >= 2 and split into independent <delegate_task> workstreams.",
|
|
8432
|
+
'- Prefer existing custom agents for delegated work when suitable (use <delegate_task agent="name" ...>).',
|
|
8433
|
+
"- If no existing custom agent fits, create focused delegate streams via profile-based <delegate_task> blocks.",
|
|
8434
|
+
"- If single-agent execution is still chosen, include one line: DELEGATION_IMPOSSIBLE: <reason>.",
|
|
8435
|
+
]
|
|
8436
|
+
: []),
|
|
8216
8437
|
"</orchestrate>",
|
|
8217
8438
|
].join("\n");
|
|
8218
8439
|
await this.session.prompt(mentionPrompt, {
|
|
@@ -8385,6 +8606,1104 @@ export class InteractiveMode {
|
|
|
8385
8606
|
}
|
|
8386
8607
|
return { targetIndex, maxIterations, forceInit };
|
|
8387
8608
|
}
|
|
8609
|
+
getSwarmHelpText() {
|
|
8610
|
+
return [
|
|
8611
|
+
"Usage:",
|
|
8612
|
+
" /swarm run <task> [--max-parallel N] [--budget-usd X]",
|
|
8613
|
+
" /swarm from-singular <run-id> --option <1|2|3> [--max-parallel N] [--budget-usd X]",
|
|
8614
|
+
" /swarm watch [run-id]",
|
|
8615
|
+
" /swarm retry <run-id> <task-id> [--reset-brief]",
|
|
8616
|
+
" /swarm resume <run-id>",
|
|
8617
|
+
" /swarm help",
|
|
8618
|
+
"",
|
|
8619
|
+
"Consistency model:",
|
|
8620
|
+
" Scopes -> Touches -> Locks -> Gates -> Done",
|
|
8621
|
+
].join("\n");
|
|
8622
|
+
}
|
|
8623
|
+
buildSwarmRecommendationFromOrchestrate(parsed) {
|
|
8624
|
+
const reasons = [];
|
|
8625
|
+
let score = 0;
|
|
8626
|
+
const task = parsed.task.replace(/\s+/g, " ").trim();
|
|
8627
|
+
const normalizedTask = task.toLowerCase();
|
|
8628
|
+
const dependencyEdges = parsed.dependencies?.reduce((sum, entry) => sum + entry.dependsOn.length, 0) ?? 0;
|
|
8629
|
+
const effectiveParallel = parsed.mode === "parallel" ? parsed.maxParallel ?? parsed.agents : 1;
|
|
8630
|
+
const highRiskPattern = /\b(refactor|rewrite|migration|migrate|breaking|rollback|security|auth|authentication|authorization|permission|payment|billing|schema|database|critical)\b/i;
|
|
8631
|
+
const mediumRiskPattern = /\b(cross[-\s]?module|architecture|infra|platform|multi[-\s]?file|integration)\b/i;
|
|
8632
|
+
if (highRiskPattern.test(normalizedTask)) {
|
|
8633
|
+
score += 2;
|
|
8634
|
+
reasons.push("task has high-risk keywords");
|
|
8635
|
+
}
|
|
8636
|
+
else if (mediumRiskPattern.test(normalizedTask)) {
|
|
8637
|
+
score += 1;
|
|
8638
|
+
reasons.push("task has architecture/cross-module scope");
|
|
8639
|
+
}
|
|
8640
|
+
if (parsed.agents >= 4) {
|
|
8641
|
+
score += 2;
|
|
8642
|
+
reasons.push(`high agent count (${parsed.agents})`);
|
|
8643
|
+
}
|
|
8644
|
+
else if (parsed.agents >= 3) {
|
|
8645
|
+
score += 1;
|
|
8646
|
+
reasons.push(`multi-agent run (${parsed.agents})`);
|
|
8647
|
+
}
|
|
8648
|
+
if (dependencyEdges >= 3) {
|
|
8649
|
+
score += 2;
|
|
8650
|
+
reasons.push(`complex dependency graph (${dependencyEdges} edges)`);
|
|
8651
|
+
}
|
|
8652
|
+
else if (dependencyEdges > 0) {
|
|
8653
|
+
score += 1;
|
|
8654
|
+
reasons.push(`dependency graph present (${dependencyEdges} edges)`);
|
|
8655
|
+
}
|
|
8656
|
+
if (effectiveParallel >= 3) {
|
|
8657
|
+
score += 1;
|
|
8658
|
+
reasons.push(`high parallelism (${effectiveParallel})`);
|
|
8659
|
+
}
|
|
8660
|
+
if ((parsed.locks?.length ?? 0) > 0) {
|
|
8661
|
+
score += 1;
|
|
8662
|
+
reasons.push("explicit lock coordination requested");
|
|
8663
|
+
}
|
|
8664
|
+
if (task.length >= 180) {
|
|
8665
|
+
score += 1;
|
|
8666
|
+
reasons.push("long task brief");
|
|
8667
|
+
}
|
|
8668
|
+
const safeTask = task.replace(/"/g, "'");
|
|
8669
|
+
const commandParts = [`/swarm run "${safeTask}"`];
|
|
8670
|
+
if (parsed.maxParallel !== undefined && parsed.maxParallel > 0) {
|
|
8671
|
+
commandParts.push(`--max-parallel ${parsed.maxParallel}`);
|
|
8672
|
+
}
|
|
8673
|
+
return {
|
|
8674
|
+
recommend: score >= 3,
|
|
8675
|
+
reasons,
|
|
8676
|
+
command: commandParts.join(" "),
|
|
8677
|
+
};
|
|
8678
|
+
}
|
|
8679
|
+
isEffectiveContractReady(contract) {
|
|
8680
|
+
const hasText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
8681
|
+
const hasList = (value) => Array.isArray(value) && value.some((item) => item.trim().length > 0);
|
|
8682
|
+
return (hasText(contract.goal) ||
|
|
8683
|
+
hasList(contract.scope_include) ||
|
|
8684
|
+
hasList(contract.constraints) ||
|
|
8685
|
+
hasList(contract.quality_gates) ||
|
|
8686
|
+
hasList(contract.definition_of_done));
|
|
8687
|
+
}
|
|
8688
|
+
parseContractListInput(raw) {
|
|
8689
|
+
if (!raw)
|
|
8690
|
+
return [];
|
|
8691
|
+
return raw
|
|
8692
|
+
.split(/[\n;,]+/)
|
|
8693
|
+
.map((item) => item.trim())
|
|
8694
|
+
.filter((item) => item.length > 0);
|
|
8695
|
+
}
|
|
8696
|
+
buildAutoDraftContractFromTask(task) {
|
|
8697
|
+
const normalizedTask = task.replace(/\s+/g, " ").trim();
|
|
8698
|
+
const hints = loadProjectIndex(this.sessionManager.getCwd());
|
|
8699
|
+
const matchedFiles = hints ? queryProjectIndex(hints, task, 8).matches.map((entry) => entry.path) : [];
|
|
8700
|
+
const scopeInclude = matchedFiles.length > 0 ? matchedFiles.map((filePath) => filePath.split("/").slice(0, 2).join("/")).slice(0, 6) : [];
|
|
8701
|
+
return normalizeEngineeringContract({
|
|
8702
|
+
goal: normalizedTask.length > 0 ? normalizedTask : "Deliver requested change safely with bounded blast radius.",
|
|
8703
|
+
scope_include: scopeInclude.length > 0 ? [...new Set(scopeInclude)].map((scope) => `${scope}/**`) : ["src/**", "test/**"],
|
|
8704
|
+
scope_exclude: ["node_modules/**", "dist/**", ".iosm/**"],
|
|
8705
|
+
constraints: [
|
|
8706
|
+
"Preserve backward compatibility unless explicitly approved.",
|
|
8707
|
+
"Keep changes scoped to declared touch zones.",
|
|
8708
|
+
],
|
|
8709
|
+
quality_gates: [
|
|
8710
|
+
"Targeted tests for touched modules pass.",
|
|
8711
|
+
"Lint/type checks pass for changed files.",
|
|
8712
|
+
],
|
|
8713
|
+
definition_of_done: [
|
|
8714
|
+
"Implementation merged with verification evidence.",
|
|
8715
|
+
"Risk notes and rollback path documented.",
|
|
8716
|
+
],
|
|
8717
|
+
});
|
|
8718
|
+
}
|
|
8719
|
+
async runSwarmContractGuidedInterview(task) {
|
|
8720
|
+
const goal = await this.showExtensionInput("Swarm contract: goal (required)", task.trim() || "Deliver requested change safely.");
|
|
8721
|
+
if (goal === undefined)
|
|
8722
|
+
return undefined;
|
|
8723
|
+
const scopeInclude = await this.showExtensionInput("Swarm contract: scope_include (comma/newline separated)", "src/**, test/**");
|
|
8724
|
+
if (scopeInclude === undefined)
|
|
8725
|
+
return undefined;
|
|
8726
|
+
const scopeExclude = await this.showExtensionInput("Swarm contract: scope_exclude (comma/newline separated)", "node_modules/**, dist/**, .iosm/**");
|
|
8727
|
+
if (scopeExclude === undefined)
|
|
8728
|
+
return undefined;
|
|
8729
|
+
const constraints = await this.showExtensionInput("Swarm contract: constraints (comma/newline separated)", "no breaking API changes; no unrelated refactors");
|
|
8730
|
+
if (constraints === undefined)
|
|
8731
|
+
return undefined;
|
|
8732
|
+
const gates = await this.showExtensionInput("Swarm contract: quality_gates (comma/newline separated)", "targeted tests pass; lint/type checks pass");
|
|
8733
|
+
if (gates === undefined)
|
|
8734
|
+
return undefined;
|
|
8735
|
+
const done = await this.showExtensionInput("Swarm contract: definition_of_done (comma/newline separated)", "implementation complete; validation evidence attached");
|
|
8736
|
+
if (done === undefined)
|
|
8737
|
+
return undefined;
|
|
8738
|
+
return normalizeEngineeringContract({
|
|
8739
|
+
goal: goal.trim(),
|
|
8740
|
+
scope_include: this.parseContractListInput(scopeInclude),
|
|
8741
|
+
scope_exclude: this.parseContractListInput(scopeExclude),
|
|
8742
|
+
constraints: this.parseContractListInput(constraints),
|
|
8743
|
+
quality_gates: this.parseContractListInput(gates),
|
|
8744
|
+
definition_of_done: this.parseContractListInput(done),
|
|
8745
|
+
});
|
|
8746
|
+
}
|
|
8747
|
+
async ensureSwarmEffectiveContract(task) {
|
|
8748
|
+
const initialState = this.getContractStateSafe();
|
|
8749
|
+
if (!initialState)
|
|
8750
|
+
return undefined;
|
|
8751
|
+
if (this.isEffectiveContractReady(initialState.effective)) {
|
|
8752
|
+
return initialState.effective;
|
|
8753
|
+
}
|
|
8754
|
+
while (true) {
|
|
8755
|
+
const selected = await this.showExtensionSelector([
|
|
8756
|
+
"/swarm requires an effective /contract before execution can start.",
|
|
8757
|
+
"Choose how to bootstrap contract policy:",
|
|
8758
|
+
].join("\n"), [
|
|
8759
|
+
"Auto-draft from task (Recommended)",
|
|
8760
|
+
"Guided Q&A",
|
|
8761
|
+
"Open manual /contract editor",
|
|
8762
|
+
"Cancel",
|
|
8763
|
+
]);
|
|
8764
|
+
if (!selected || selected === "Cancel") {
|
|
8765
|
+
this.showStatus("Swarm cancelled: contract bootstrap not completed.");
|
|
8766
|
+
return undefined;
|
|
8767
|
+
}
|
|
8768
|
+
if (selected.startsWith("Auto-draft")) {
|
|
8769
|
+
try {
|
|
8770
|
+
const draft = this.buildAutoDraftContractFromTask(task);
|
|
8771
|
+
this.contractService.save("session", draft);
|
|
8772
|
+
this.syncRuntimePromptSuffix();
|
|
8773
|
+
this.showStatus("Swarm contract bootstrap: auto-draft saved to session overlay.");
|
|
8774
|
+
}
|
|
8775
|
+
catch (error) {
|
|
8776
|
+
this.showWarning(error instanceof Error ? error.message : String(error));
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
else if (selected === "Guided Q&A") {
|
|
8780
|
+
const drafted = await this.runSwarmContractGuidedInterview(task);
|
|
8781
|
+
if (!drafted) {
|
|
8782
|
+
this.showStatus("Swarm contract interview cancelled.");
|
|
8783
|
+
}
|
|
8784
|
+
else {
|
|
8785
|
+
try {
|
|
8786
|
+
this.contractService.save("session", drafted);
|
|
8787
|
+
this.syncRuntimePromptSuffix();
|
|
8788
|
+
this.showStatus("Swarm contract bootstrap: guided contract saved to session overlay.");
|
|
8789
|
+
}
|
|
8790
|
+
catch (error) {
|
|
8791
|
+
this.showWarning(error instanceof Error ? error.message : String(error));
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
else if (selected === "Open manual /contract editor") {
|
|
8796
|
+
await this.runContractInteractiveMenu();
|
|
8797
|
+
}
|
|
8798
|
+
const state = this.getContractStateSafe();
|
|
8799
|
+
if (!state)
|
|
8800
|
+
return undefined;
|
|
8801
|
+
if (this.isEffectiveContractReady(state.effective)) {
|
|
8802
|
+
return state.effective;
|
|
8803
|
+
}
|
|
8804
|
+
this.showWarning("Effective contract is still empty. Swarm execution remains blocked.");
|
|
8805
|
+
}
|
|
8806
|
+
}
|
|
8807
|
+
async maybeWarnSwarmSemantic(scaleMode) {
|
|
8808
|
+
if (scaleMode === "small") {
|
|
8809
|
+
return "optional_for_small_repo";
|
|
8810
|
+
}
|
|
8811
|
+
try {
|
|
8812
|
+
const status = await this.createSemanticRuntime().status();
|
|
8813
|
+
if (!status.configured) {
|
|
8814
|
+
this.showWarning("Swarm recommendation: configure semantic index via /semantic setup and /semantic index.");
|
|
8815
|
+
return "not_configured";
|
|
8816
|
+
}
|
|
8817
|
+
if (!status.enabled) {
|
|
8818
|
+
this.showWarning("Swarm recommendation: enable semantic index in /semantic setup for medium/large repositories.");
|
|
8819
|
+
return "configured_but_disabled";
|
|
8820
|
+
}
|
|
8821
|
+
if (!status.indexed) {
|
|
8822
|
+
this.showWarning("Swarm recommendation: run /semantic index before long swarm runs.");
|
|
8823
|
+
return "configured_not_indexed";
|
|
8824
|
+
}
|
|
8825
|
+
if (status.stale) {
|
|
8826
|
+
const action = status.staleReason === "provider_changed" || status.staleReason === "dimension_mismatch" ? "/semantic rebuild" : "/semantic index";
|
|
8827
|
+
this.showWarning(`Swarm recommendation: semantic index is stale (${status.staleReason ?? "unknown"}). Run ${action}.`);
|
|
8828
|
+
return `stale:${status.staleReason ?? "unknown"}`;
|
|
8829
|
+
}
|
|
8830
|
+
return `ready:${status.provider}/${status.model}`;
|
|
8831
|
+
}
|
|
8832
|
+
catch (error) {
|
|
8833
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8834
|
+
this.showWarning(`Swarm recommendation: semantic status unavailable (${message}). Use /semantic status.`);
|
|
8835
|
+
return `status_unavailable:${message}`;
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
ensureSwarmProjectIndex(task) {
|
|
8839
|
+
const cwd = this.sessionManager.getCwd();
|
|
8840
|
+
const existing = loadProjectIndex(cwd);
|
|
8841
|
+
if (existing) {
|
|
8842
|
+
if (existing.meta.repoScaleMode === "small") {
|
|
8843
|
+
return { index: existing, scaleMode: "small", rebuilt: false };
|
|
8844
|
+
}
|
|
8845
|
+
const ensured = ensureProjectIndex(cwd, existing.meta.repoScaleMode);
|
|
8846
|
+
return { index: ensured.index, scaleMode: ensured.index.meta.repoScaleMode, rebuilt: ensured.rebuilt };
|
|
8847
|
+
}
|
|
8848
|
+
const quick = buildProjectIndex(cwd, { maxFiles: 6_000 });
|
|
8849
|
+
const scaleMode = quick.meta.repoScaleMode;
|
|
8850
|
+
if (scaleMode === "small") {
|
|
8851
|
+
return { index: quick, scaleMode, rebuilt: true };
|
|
8852
|
+
}
|
|
8853
|
+
saveProjectIndex(cwd, quick);
|
|
8854
|
+
return { index: quick, scaleMode, rebuilt: true };
|
|
8855
|
+
}
|
|
8856
|
+
parseSwarmCommand(text) {
|
|
8857
|
+
const args = this.parseSlashArgs(text).slice(1);
|
|
8858
|
+
if (args.length === 0) {
|
|
8859
|
+
return { subcommand: "help" };
|
|
8860
|
+
}
|
|
8861
|
+
const subcommand = (args[0] ?? "").toLowerCase();
|
|
8862
|
+
const rest = args.slice(1);
|
|
8863
|
+
if (subcommand === "help" || subcommand === "-h" || subcommand === "--help") {
|
|
8864
|
+
return { subcommand: "help" };
|
|
8865
|
+
}
|
|
8866
|
+
if (subcommand === "watch") {
|
|
8867
|
+
return {
|
|
8868
|
+
subcommand: "watch",
|
|
8869
|
+
runId: rest[0],
|
|
8870
|
+
};
|
|
8871
|
+
}
|
|
8872
|
+
if (subcommand === "resume") {
|
|
8873
|
+
const runId = rest[0];
|
|
8874
|
+
if (!runId) {
|
|
8875
|
+
this.showWarning("Usage: /swarm resume <run-id>");
|
|
8876
|
+
return undefined;
|
|
8877
|
+
}
|
|
8878
|
+
return { subcommand: "resume", runId };
|
|
8879
|
+
}
|
|
8880
|
+
if (subcommand === "retry") {
|
|
8881
|
+
const runId = rest[0];
|
|
8882
|
+
const taskId = rest[1];
|
|
8883
|
+
if (!runId || !taskId) {
|
|
8884
|
+
this.showWarning("Usage: /swarm retry <run-id> <task-id> [--reset-brief]");
|
|
8885
|
+
return undefined;
|
|
8886
|
+
}
|
|
8887
|
+
const resetBrief = rest.slice(2).some((token) => token === "--reset-brief");
|
|
8888
|
+
return { subcommand: "retry", runId, taskId, resetBrief };
|
|
8889
|
+
}
|
|
8890
|
+
let maxParallel;
|
|
8891
|
+
let budgetUsd;
|
|
8892
|
+
const taskParts = [];
|
|
8893
|
+
let fromSingularOption;
|
|
8894
|
+
let fromSingularRunId;
|
|
8895
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
8896
|
+
const token = rest[index] ?? "";
|
|
8897
|
+
if (token === "--max-parallel") {
|
|
8898
|
+
const next = rest[index + 1];
|
|
8899
|
+
const parsed = next ? Number.parseInt(next, 10) : Number.NaN;
|
|
8900
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
|
|
8901
|
+
this.showWarning("Invalid --max-parallel value (expected 1..20).");
|
|
8902
|
+
return undefined;
|
|
8903
|
+
}
|
|
8904
|
+
maxParallel = parsed;
|
|
8905
|
+
index += 1;
|
|
8906
|
+
continue;
|
|
8907
|
+
}
|
|
8908
|
+
if (token === "--budget-usd") {
|
|
8909
|
+
const next = rest[index + 1];
|
|
8910
|
+
const parsed = next ? Number.parseFloat(next) : Number.NaN;
|
|
8911
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
8912
|
+
this.showWarning("Invalid --budget-usd value (expected > 0).");
|
|
8913
|
+
return undefined;
|
|
8914
|
+
}
|
|
8915
|
+
budgetUsd = parsed;
|
|
8916
|
+
index += 1;
|
|
8917
|
+
continue;
|
|
8918
|
+
}
|
|
8919
|
+
if (token === "--option") {
|
|
8920
|
+
const next = rest[index + 1];
|
|
8921
|
+
const parsed = next ? Number.parseInt(next, 10) : Number.NaN;
|
|
8922
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 3) {
|
|
8923
|
+
this.showWarning("Invalid --option value (expected 1|2|3).");
|
|
8924
|
+
return undefined;
|
|
8925
|
+
}
|
|
8926
|
+
fromSingularOption = parsed;
|
|
8927
|
+
index += 1;
|
|
8928
|
+
continue;
|
|
8929
|
+
}
|
|
8930
|
+
if (token.startsWith("-")) {
|
|
8931
|
+
this.showWarning(`Unknown option for /swarm ${subcommand}: ${token}`);
|
|
8932
|
+
return undefined;
|
|
8933
|
+
}
|
|
8934
|
+
if (subcommand === "from-singular" && !fromSingularRunId) {
|
|
8935
|
+
fromSingularRunId = token;
|
|
8936
|
+
continue;
|
|
8937
|
+
}
|
|
8938
|
+
taskParts.push(token);
|
|
8939
|
+
}
|
|
8940
|
+
if (subcommand === "run") {
|
|
8941
|
+
const task = taskParts.join(" ").trim();
|
|
8942
|
+
if (!task) {
|
|
8943
|
+
this.showWarning("Usage: /swarm run <task> [--max-parallel N] [--budget-usd X]");
|
|
8944
|
+
return undefined;
|
|
8945
|
+
}
|
|
8946
|
+
return { subcommand, task, maxParallel, budgetUsd };
|
|
8947
|
+
}
|
|
8948
|
+
if (subcommand === "from-singular") {
|
|
8949
|
+
if (!fromSingularRunId || !fromSingularOption) {
|
|
8950
|
+
this.showWarning("Usage: /swarm from-singular <run-id> --option <1|2|3> [--max-parallel N] [--budget-usd X]");
|
|
8951
|
+
return undefined;
|
|
8952
|
+
}
|
|
8953
|
+
return {
|
|
8954
|
+
subcommand,
|
|
8955
|
+
runId: fromSingularRunId,
|
|
8956
|
+
option: fromSingularOption,
|
|
8957
|
+
maxParallel,
|
|
8958
|
+
budgetUsd,
|
|
8959
|
+
};
|
|
8960
|
+
}
|
|
8961
|
+
this.showWarning(`Unknown /swarm subcommand "${subcommand}". Use /swarm help.`);
|
|
8962
|
+
return undefined;
|
|
8963
|
+
}
|
|
8964
|
+
buildSwarmBootstrapState(runId, plan, budgetUsd) {
|
|
8965
|
+
const tasks = {};
|
|
8966
|
+
for (const task of plan.tasks) {
|
|
8967
|
+
tasks[task.id] = {
|
|
8968
|
+
id: task.id,
|
|
8969
|
+
status: task.depends_on.length === 0 ? "ready" : "pending",
|
|
8970
|
+
attempts: 0,
|
|
8971
|
+
depends_on: [...task.depends_on],
|
|
8972
|
+
touches: [...task.touches],
|
|
8973
|
+
scopes: [...task.scopes],
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
return {
|
|
8977
|
+
runId,
|
|
8978
|
+
status: "running",
|
|
8979
|
+
createdAt: new Date().toISOString(),
|
|
8980
|
+
updatedAt: new Date().toISOString(),
|
|
8981
|
+
tick: 0,
|
|
8982
|
+
noProgressTicks: 0,
|
|
8983
|
+
readyQueue: Object.values(tasks)
|
|
8984
|
+
.filter((task) => task.status === "ready")
|
|
8985
|
+
.map((task) => task.id),
|
|
8986
|
+
blockedTasks: [],
|
|
8987
|
+
tasks,
|
|
8988
|
+
locks: {},
|
|
8989
|
+
retries: {},
|
|
8990
|
+
budget: {
|
|
8991
|
+
limitUsd: budgetUsd,
|
|
8992
|
+
spentUsd: 0,
|
|
8993
|
+
warned80: false,
|
|
8994
|
+
hardStopped: false,
|
|
8995
|
+
},
|
|
8996
|
+
};
|
|
8997
|
+
}
|
|
8998
|
+
buildSwarmRunMeta(input) {
|
|
8999
|
+
return {
|
|
9000
|
+
runId: input.runId,
|
|
9001
|
+
createdAt: new Date().toISOString(),
|
|
9002
|
+
source: input.source,
|
|
9003
|
+
request: input.request,
|
|
9004
|
+
contract: input.contract,
|
|
9005
|
+
contractHash: crypto.createHash("sha256").update(JSON.stringify(input.contract)).digest("hex").slice(0, 16),
|
|
9006
|
+
repoScaleMode: input.repoScaleMode,
|
|
9007
|
+
semanticStatus: input.semanticStatus,
|
|
9008
|
+
maxParallel: input.maxParallel,
|
|
9009
|
+
budgetUsd: input.budgetUsd,
|
|
9010
|
+
linkedSingularRunId: input.linkedSingularRunId,
|
|
9011
|
+
linkedSingularOption: input.linkedSingularOption,
|
|
9012
|
+
};
|
|
9013
|
+
}
|
|
9014
|
+
resolveSwarmTaskProfile(task) {
|
|
9015
|
+
if (task.concurrency_class === "analysis")
|
|
9016
|
+
return "explore";
|
|
9017
|
+
if (task.concurrency_class === "verification" || task.concurrency_class === "tests")
|
|
9018
|
+
return "iosm_verifier";
|
|
9019
|
+
if (task.concurrency_class === "docs")
|
|
9020
|
+
return "plan";
|
|
9021
|
+
return "full";
|
|
9022
|
+
}
|
|
9023
|
+
estimateSwarmTaskCostUsd(task) {
|
|
9024
|
+
if (task.severity === "high")
|
|
9025
|
+
return 0.35;
|
|
9026
|
+
if (task.severity === "medium")
|
|
9027
|
+
return 0.2;
|
|
9028
|
+
return 0.12;
|
|
9029
|
+
}
|
|
9030
|
+
deriveSwarmTaskDelegateParallelHint(task) {
|
|
9031
|
+
const brief = task.brief.toLowerCase();
|
|
9032
|
+
const hasComplexKeyword = /(refactor|migration|rewrite|split|cross[-\s]?module|architecture|platform|integration|security|auth)/i.test(brief);
|
|
9033
|
+
const hasVeryComplexSignal = /(overhaul|major|system-wide|cross-cutting|multi-service|facet|registry)/i.test(brief) ||
|
|
9034
|
+
task.touches.length >= 5 ||
|
|
9035
|
+
task.scopes.length >= 4;
|
|
9036
|
+
if (task.severity === "low" && task.touches.length <= 2 && task.scopes.length <= 2 && !hasComplexKeyword) {
|
|
9037
|
+
return 1;
|
|
9038
|
+
}
|
|
9039
|
+
if (task.severity === "high" && (hasVeryComplexSignal || hasComplexKeyword || task.touches.length >= 3)) {
|
|
9040
|
+
return 10;
|
|
9041
|
+
}
|
|
9042
|
+
if (task.severity === "high")
|
|
9043
|
+
return 8;
|
|
9044
|
+
if (task.severity === "medium" && (hasVeryComplexSignal || task.touches.length >= 4 || hasComplexKeyword))
|
|
9045
|
+
return 7;
|
|
9046
|
+
if (task.severity === "medium")
|
|
9047
|
+
return 5;
|
|
9048
|
+
return hasComplexKeyword ? 3 : 1;
|
|
9049
|
+
}
|
|
9050
|
+
parseSwarmSpawnCandidates(output, parentTaskId) {
|
|
9051
|
+
const lines = output.split(/\r?\n/);
|
|
9052
|
+
const results = [];
|
|
9053
|
+
for (const rawLine of lines) {
|
|
9054
|
+
const line = rawLine.trim();
|
|
9055
|
+
const match = line.match(/^[*-]\s+(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(low|medium|high)\s*$/i);
|
|
9056
|
+
if (!match)
|
|
9057
|
+
continue;
|
|
9058
|
+
results.push({
|
|
9059
|
+
description: match[1].trim(),
|
|
9060
|
+
path: match[2].trim(),
|
|
9061
|
+
changeType: match[3].trim(),
|
|
9062
|
+
severity: match[4].trim().toLowerCase(),
|
|
9063
|
+
parentTaskId,
|
|
9064
|
+
});
|
|
9065
|
+
if (results.length >= 10)
|
|
9066
|
+
break;
|
|
9067
|
+
}
|
|
9068
|
+
return results;
|
|
9069
|
+
}
|
|
9070
|
+
loadSingularAnalysisByRunId(runId) {
|
|
9071
|
+
const analysisPath = path.join(this.singularService.getAnalysesRoot(), runId, "analysis.json");
|
|
9072
|
+
if (!fs.existsSync(analysisPath))
|
|
9073
|
+
return undefined;
|
|
9074
|
+
try {
|
|
9075
|
+
return JSON.parse(fs.readFileSync(analysisPath, "utf8"));
|
|
9076
|
+
}
|
|
9077
|
+
catch {
|
|
9078
|
+
return undefined;
|
|
9079
|
+
}
|
|
9080
|
+
}
|
|
9081
|
+
async dispatchSwarmTaskWithAgent(input) {
|
|
9082
|
+
const model = this.session.model;
|
|
9083
|
+
if (!model) {
|
|
9084
|
+
return {
|
|
9085
|
+
taskId: input.task.id,
|
|
9086
|
+
status: "error",
|
|
9087
|
+
error: "No active model configured for swarm task dispatch.",
|
|
9088
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9089
|
+
};
|
|
9090
|
+
}
|
|
9091
|
+
const profile = this.resolveSwarmTaskProfile(input.task);
|
|
9092
|
+
const delegateParallelHint = this.deriveSwarmTaskDelegateParallelHint(input.task);
|
|
9093
|
+
const safeDescription = input.task.brief.replace(/\s+/g, " ").trim().slice(0, 120).replace(/"/g, "'");
|
|
9094
|
+
const prompt = [
|
|
9095
|
+
`<swarm_task run_id="${input.meta.runId}" task_id="${input.task.id}" profile_hint="${profile}">`,
|
|
9096
|
+
`request: ${input.meta.request}`,
|
|
9097
|
+
`task_brief: ${input.task.brief}`,
|
|
9098
|
+
`touches: ${input.runtime.touches.join(", ") || "(none)"}`,
|
|
9099
|
+
`scopes: ${input.runtime.scopes.join(", ") || "(none)"}`,
|
|
9100
|
+
`constraints: ${(input.contract.constraints ?? []).join("; ") || "(none)"}`,
|
|
9101
|
+
`quality_gates: ${(input.contract.quality_gates ?? []).join("; ") || "(none)"}`,
|
|
9102
|
+
"",
|
|
9103
|
+
"Execution requirements:",
|
|
9104
|
+
`- 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}".`,
|
|
9105
|
+
`- Inside this single task call, prefer decomposition into <delegate_task> subtasks when delegate_parallel_hint >= 2 (target parallel fan-out up to ${delegateParallelHint}).`,
|
|
9106
|
+
"- If decomposition is not beneficial, continue with single-agent execution (optional note: DELEGATION_IMPOSSIBLE: <reason>).",
|
|
9107
|
+
"- Keep edits inside declared scopes/touches. If scope expansion is required, explain and stop.",
|
|
9108
|
+
"- If blocked, respond with line: BLOCKED: <reason>",
|
|
9109
|
+
"- Optional spawn candidates format: '- <description> | <path> | <change_type> | <low|medium|high>'",
|
|
9110
|
+
"</swarm_task>",
|
|
9111
|
+
].join("\n");
|
|
9112
|
+
const cwd = this.sessionManager.getCwd();
|
|
9113
|
+
const agentDir = getAgentDir();
|
|
9114
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
9115
|
+
const authStorage = this.session.modelRegistry.authStorage;
|
|
9116
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
9117
|
+
cwd,
|
|
9118
|
+
agentDir,
|
|
9119
|
+
settingsManager,
|
|
9120
|
+
noExtensions: true,
|
|
9121
|
+
noSkills: true,
|
|
9122
|
+
noPromptTemplates: true,
|
|
9123
|
+
});
|
|
9124
|
+
try {
|
|
9125
|
+
await resourceLoader.reload();
|
|
9126
|
+
}
|
|
9127
|
+
catch (error) {
|
|
9128
|
+
return {
|
|
9129
|
+
taskId: input.task.id,
|
|
9130
|
+
status: "error",
|
|
9131
|
+
error: `Failed to initialize swarm task runtime: ${error instanceof Error ? error.message : String(error)}`,
|
|
9132
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9133
|
+
};
|
|
9134
|
+
}
|
|
9135
|
+
let swarmSession;
|
|
9136
|
+
try {
|
|
9137
|
+
const created = await createAgentSession({
|
|
9138
|
+
cwd,
|
|
9139
|
+
sessionManager: SessionManager.inMemory(),
|
|
9140
|
+
settingsManager,
|
|
9141
|
+
authStorage,
|
|
9142
|
+
modelRegistry: this.session.modelRegistry,
|
|
9143
|
+
resourceLoader,
|
|
9144
|
+
model,
|
|
9145
|
+
thinkingLevel: this.session.thinkingLevel,
|
|
9146
|
+
profile: "full",
|
|
9147
|
+
enableTaskTool: true,
|
|
9148
|
+
});
|
|
9149
|
+
swarmSession = created.session;
|
|
9150
|
+
}
|
|
9151
|
+
catch (error) {
|
|
9152
|
+
return {
|
|
9153
|
+
taskId: input.task.id,
|
|
9154
|
+
status: "error",
|
|
9155
|
+
error: `Failed to create isolated swarm session: ${error instanceof Error ? error.message : String(error)}`,
|
|
9156
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9157
|
+
};
|
|
9158
|
+
}
|
|
9159
|
+
let taskToolCalls = 0;
|
|
9160
|
+
const taskErrors = [];
|
|
9161
|
+
let delegatedFailed = 0;
|
|
9162
|
+
let delegatedTasks = 0;
|
|
9163
|
+
const delegatedFailureCauses = new Map();
|
|
9164
|
+
const accumulateFailureCauses = (raw) => {
|
|
9165
|
+
if (!raw || typeof raw !== "object")
|
|
9166
|
+
return;
|
|
9167
|
+
for (const [cause, count] of Object.entries(raw)) {
|
|
9168
|
+
if (typeof cause !== "string" || !cause.trim())
|
|
9169
|
+
continue;
|
|
9170
|
+
const numeric = typeof count === "number" ? count : Number.parseInt(String(count ?? 0), 10);
|
|
9171
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
9172
|
+
continue;
|
|
9173
|
+
delegatedFailureCauses.set(cause, (delegatedFailureCauses.get(cause) ?? 0) + numeric);
|
|
9174
|
+
}
|
|
9175
|
+
};
|
|
9176
|
+
const chunks = [];
|
|
9177
|
+
const unsubscribe = swarmSession.subscribe((event) => {
|
|
9178
|
+
if (event.type === "tool_execution_start" && event.toolName === "task") {
|
|
9179
|
+
taskToolCalls += 1;
|
|
9180
|
+
return;
|
|
9181
|
+
}
|
|
9182
|
+
if (event.type === "tool_execution_end" && event.toolName === "task") {
|
|
9183
|
+
const result = event.result;
|
|
9184
|
+
const details = result?.details;
|
|
9185
|
+
if (typeof details?.delegatedFailed === "number" && Number.isFinite(details.delegatedFailed)) {
|
|
9186
|
+
delegatedFailed += Math.max(0, details.delegatedFailed);
|
|
9187
|
+
}
|
|
9188
|
+
if (typeof details?.delegatedTasks === "number" && Number.isFinite(details.delegatedTasks)) {
|
|
9189
|
+
delegatedTasks += Math.max(0, details.delegatedTasks);
|
|
9190
|
+
}
|
|
9191
|
+
accumulateFailureCauses(details?.failureCauses);
|
|
9192
|
+
if (event.isError) {
|
|
9193
|
+
taskErrors.push(result?.error ?? result?.output ?? "task tool failed");
|
|
9194
|
+
}
|
|
9195
|
+
return;
|
|
9196
|
+
}
|
|
9197
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
9198
|
+
for (const part of event.message.content) {
|
|
9199
|
+
if (part.type === "text" && part.text.trim()) {
|
|
9200
|
+
chunks.push(part.text.trim());
|
|
9201
|
+
}
|
|
9202
|
+
}
|
|
9203
|
+
}
|
|
9204
|
+
});
|
|
9205
|
+
try {
|
|
9206
|
+
await swarmSession.prompt(prompt, {
|
|
9207
|
+
expandPromptTemplates: false,
|
|
9208
|
+
skipIosmAutopilot: true,
|
|
9209
|
+
skipOrchestrationDirective: true,
|
|
9210
|
+
source: "interactive",
|
|
9211
|
+
});
|
|
9212
|
+
}
|
|
9213
|
+
catch (error) {
|
|
9214
|
+
return {
|
|
9215
|
+
taskId: input.task.id,
|
|
9216
|
+
status: "error",
|
|
9217
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9218
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9219
|
+
};
|
|
9220
|
+
}
|
|
9221
|
+
finally {
|
|
9222
|
+
unsubscribe();
|
|
9223
|
+
if (swarmSession.isStreaming) {
|
|
9224
|
+
await swarmSession.abort().catch(() => {
|
|
9225
|
+
// best effort
|
|
9226
|
+
});
|
|
9227
|
+
}
|
|
9228
|
+
swarmSession.dispose();
|
|
9229
|
+
}
|
|
9230
|
+
const output = chunks.join("\n\n").trim();
|
|
9231
|
+
if (/^\s*BLOCKED\s*:/im.test(output)) {
|
|
9232
|
+
const reason = output.match(/^\s*BLOCKED\s*:\s*(.+)$/im)?.[1]?.trim() ?? "Blocked by execution policy.";
|
|
9233
|
+
return {
|
|
9234
|
+
taskId: input.task.id,
|
|
9235
|
+
status: "blocked",
|
|
9236
|
+
error: reason,
|
|
9237
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9238
|
+
};
|
|
9239
|
+
}
|
|
9240
|
+
if (taskToolCalls === 0) {
|
|
9241
|
+
return {
|
|
9242
|
+
taskId: input.task.id,
|
|
9243
|
+
status: "error",
|
|
9244
|
+
error: "No task tool call executed by assistant.",
|
|
9245
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9246
|
+
};
|
|
9247
|
+
}
|
|
9248
|
+
if (taskErrors.length > 0) {
|
|
9249
|
+
return {
|
|
9250
|
+
taskId: input.task.id,
|
|
9251
|
+
status: "error",
|
|
9252
|
+
error: taskErrors.join(" | "),
|
|
9253
|
+
failureCause: "task_tool_error",
|
|
9254
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9257
|
+
if (delegatedFailed > 0) {
|
|
9258
|
+
const failureSummary = [...delegatedFailureCauses.entries()]
|
|
9259
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
9260
|
+
.map(([cause, count]) => `${cause}=${count}`)
|
|
9261
|
+
.join(", ");
|
|
9262
|
+
const totalDelegates = delegatedTasks > 0 ? delegatedTasks : delegatedFailed;
|
|
9263
|
+
const error = `delegates_failed ${delegatedFailed}/${totalDelegates}${failureSummary ? ` (${failureSummary})` : ""}`;
|
|
9264
|
+
return {
|
|
9265
|
+
taskId: input.task.id,
|
|
9266
|
+
status: "error",
|
|
9267
|
+
error,
|
|
9268
|
+
failureCause: failureSummary || "delegates_failed",
|
|
9269
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9270
|
+
};
|
|
9271
|
+
}
|
|
9272
|
+
return {
|
|
9273
|
+
taskId: input.task.id,
|
|
9274
|
+
status: "done",
|
|
9275
|
+
costUsd: this.estimateSwarmTaskCostUsd(input.task),
|
|
9276
|
+
spawnCandidates: this.parseSwarmSpawnCandidates(output, input.task.id),
|
|
9277
|
+
};
|
|
9278
|
+
}
|
|
9279
|
+
async executeSwarmRun(input) {
|
|
9280
|
+
const cwd = this.sessionManager.getCwd();
|
|
9281
|
+
const store = new SwarmStateStore(cwd, input.runId);
|
|
9282
|
+
const initialState = input.resumeState ?? this.buildSwarmBootstrapState(input.runId, input.plan, input.budgetUsd);
|
|
9283
|
+
if (!input.resumeState) {
|
|
9284
|
+
store.init(input.meta, input.plan, initialState);
|
|
9285
|
+
}
|
|
9286
|
+
let rollingIndex = input.projectIndex;
|
|
9287
|
+
let localStopRequested = false;
|
|
9288
|
+
const refreshIncrementalIndex = () => {
|
|
9289
|
+
if (!input.enableIncrementalIndex || !rollingIndex)
|
|
9290
|
+
return;
|
|
9291
|
+
const changed = collectChangedFilesSince(rollingIndex, cwd);
|
|
9292
|
+
if (changed.length === 0)
|
|
9293
|
+
return;
|
|
9294
|
+
const rebuilt = buildProjectIndex(cwd, {
|
|
9295
|
+
incrementalFrom: rollingIndex,
|
|
9296
|
+
changedFiles: changed,
|
|
9297
|
+
maxFiles: Math.max(2_000, rollingIndex.meta.totalFiles + 1_000),
|
|
9298
|
+
});
|
|
9299
|
+
saveProjectIndex(cwd, rebuilt);
|
|
9300
|
+
rollingIndex = rebuilt;
|
|
9301
|
+
};
|
|
9302
|
+
this.swarmActiveRunId = input.runId;
|
|
9303
|
+
this.showStatus(`Swarm run started: ${input.runId}`);
|
|
9304
|
+
const schedulerResult = await runSwarmScheduler({
|
|
9305
|
+
runId: input.runId,
|
|
9306
|
+
plan: input.plan,
|
|
9307
|
+
contract: input.contract,
|
|
9308
|
+
maxParallel: input.meta.maxParallel,
|
|
9309
|
+
budgetUsd: input.budgetUsd,
|
|
9310
|
+
existingState: initialState,
|
|
9311
|
+
dispatchTask: async ({ task, runtime }) => this.dispatchSwarmTaskWithAgent({
|
|
9312
|
+
meta: input.meta,
|
|
9313
|
+
task,
|
|
9314
|
+
runtime,
|
|
9315
|
+
contract: input.contract,
|
|
9316
|
+
}),
|
|
9317
|
+
confirmSpawn: async ({ candidate, parentTask }) => {
|
|
9318
|
+
const requiresConfirmation = candidate.severity === "high" || parentTask.spawn_policy === "manual_high_risk";
|
|
9319
|
+
if (!requiresConfirmation)
|
|
9320
|
+
return true;
|
|
9321
|
+
const choice = await this.showExtensionSelector([
|
|
9322
|
+
`/swarm spawn candidate requires confirmation`,
|
|
9323
|
+
`severity=${candidate.severity} task=${parentTask.id}`,
|
|
9324
|
+
`description=${candidate.description}`,
|
|
9325
|
+
`path=${candidate.path}`,
|
|
9326
|
+
].join("\n"), ["Approve spawn", "Reject spawn (Recommended)", "Abort run"]);
|
|
9327
|
+
if (!choice || choice.startsWith("Reject")) {
|
|
9328
|
+
this.showStatus(`Swarm spawn rejected: ${candidate.description}`);
|
|
9329
|
+
return false;
|
|
9330
|
+
}
|
|
9331
|
+
if (choice === "Abort run") {
|
|
9332
|
+
localStopRequested = true;
|
|
9333
|
+
this.showWarning("Swarm run marked to stop after current scheduling step.");
|
|
9334
|
+
return false;
|
|
9335
|
+
}
|
|
9336
|
+
return true;
|
|
9337
|
+
},
|
|
9338
|
+
onEvent: (event) => {
|
|
9339
|
+
store.appendEvent(event);
|
|
9340
|
+
},
|
|
9341
|
+
onStateChanged: (state) => {
|
|
9342
|
+
store.saveState(state);
|
|
9343
|
+
store.saveCheckpoint(state);
|
|
9344
|
+
refreshIncrementalIndex();
|
|
9345
|
+
},
|
|
9346
|
+
shouldStop: () => this.shutdownRequested || localStopRequested,
|
|
9347
|
+
});
|
|
9348
|
+
this.swarmActiveRunId = undefined;
|
|
9349
|
+
const taskStates = Object.values(schedulerResult.state.tasks);
|
|
9350
|
+
const doneCount = taskStates.filter((task) => task.status === "done").length;
|
|
9351
|
+
const errorCount = taskStates.filter((task) => task.status === "error").length;
|
|
9352
|
+
const blockedCount = taskStates.filter((task) => task.status === "blocked").length;
|
|
9353
|
+
const total = taskStates.length;
|
|
9354
|
+
const reportLines = [
|
|
9355
|
+
"# Swarm Integration Report",
|
|
9356
|
+
"",
|
|
9357
|
+
`- run_id: ${input.runId}`,
|
|
9358
|
+
`- source: ${input.meta.source}`,
|
|
9359
|
+
`- request: ${input.meta.request}`,
|
|
9360
|
+
`- status: ${schedulerResult.state.status}`,
|
|
9361
|
+
`- tasks: ${doneCount}/${total} done, ${errorCount} error, ${blockedCount} blocked`,
|
|
9362
|
+
`- budget: ${schedulerResult.state.budget.spentUsd.toFixed(2)}${input.meta.budgetUsd ? ` / ${input.meta.budgetUsd.toFixed(2)}` : ""} USD`,
|
|
9363
|
+
"",
|
|
9364
|
+
"## Consistency Model",
|
|
9365
|
+
"Scopes -> Touches -> Locks -> Gates -> Done",
|
|
9366
|
+
"",
|
|
9367
|
+
"## Task Gates",
|
|
9368
|
+
...schedulerResult.taskGates.map((gate) => `- ${gate.taskId}: ${gate.pass ? "pass" : "fail"}${gate.failures.length > 0 ? ` (${gate.failures.join("; ")})` : ""}`),
|
|
9369
|
+
"",
|
|
9370
|
+
"## Run Gates",
|
|
9371
|
+
`- pass: ${schedulerResult.runGate.pass}`,
|
|
9372
|
+
...schedulerResult.runGate.failures.map((item) => `- fail: ${item}`),
|
|
9373
|
+
...schedulerResult.runGate.warnings.map((item) => `- warn: ${item}`),
|
|
9374
|
+
"",
|
|
9375
|
+
"## Spawn Backlog",
|
|
9376
|
+
...(schedulerResult.spawnBacklog.length > 0
|
|
9377
|
+
? schedulerResult.spawnBacklog.map((item) => `- ${item.description} | ${item.path} | ${item.changeType} | fp=${item.fingerprint}`)
|
|
9378
|
+
: ["- none"]),
|
|
9379
|
+
];
|
|
9380
|
+
store.writeReports({
|
|
9381
|
+
integrationReport: reportLines.join("\n"),
|
|
9382
|
+
gates: {
|
|
9383
|
+
task_gates: schedulerResult.taskGates,
|
|
9384
|
+
run_gate: schedulerResult.runGate,
|
|
9385
|
+
status: schedulerResult.state.status,
|
|
9386
|
+
},
|
|
9387
|
+
sharedContext: [
|
|
9388
|
+
"# Shared Context",
|
|
9389
|
+
"",
|
|
9390
|
+
`Run ${input.runId} finished with status: ${schedulerResult.state.status}.`,
|
|
9391
|
+
`Recommendation: ${schedulerResult.runGate.pass ? "proceed to /iosm for measurable optimization" : "resolve failed gates before /iosm"}.`,
|
|
9392
|
+
].join("\n"),
|
|
9393
|
+
});
|
|
9394
|
+
this.showCommandTextBlock("Swarm Run", [
|
|
9395
|
+
`run_id: ${input.runId}`,
|
|
9396
|
+
`status: ${schedulerResult.state.status}`,
|
|
9397
|
+
`tasks: ${doneCount}/${total} done · ${errorCount} error · ${blockedCount} blocked`,
|
|
9398
|
+
`budget_usd: ${schedulerResult.state.budget.spentUsd.toFixed(2)}${input.meta.budgetUsd ? `/${input.meta.budgetUsd.toFixed(2)}` : ""}`,
|
|
9399
|
+
`watch: /swarm watch ${input.runId}`,
|
|
9400
|
+
`resume: /swarm resume ${input.runId}`,
|
|
9401
|
+
].join("\n"));
|
|
9402
|
+
}
|
|
9403
|
+
async runSwarmFromTask(task, options) {
|
|
9404
|
+
const contract = await this.ensureSwarmEffectiveContract(task);
|
|
9405
|
+
if (!contract)
|
|
9406
|
+
return;
|
|
9407
|
+
const indexInfo = this.ensureSwarmProjectIndex(task);
|
|
9408
|
+
if (indexInfo.rebuilt) {
|
|
9409
|
+
this.showStatus(`Swarm project index ready (${indexInfo.scaleMode}).`);
|
|
9410
|
+
}
|
|
9411
|
+
const semanticStatus = await this.maybeWarnSwarmSemantic(indexInfo.scaleMode);
|
|
9412
|
+
const plan = buildSwarmPlanFromTask({
|
|
9413
|
+
request: task,
|
|
9414
|
+
contract,
|
|
9415
|
+
index: indexInfo.index,
|
|
9416
|
+
});
|
|
9417
|
+
const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
9418
|
+
const maxParallel = Math.max(1, Math.min(20, options.maxParallel ?? 3));
|
|
9419
|
+
const meta = this.buildSwarmRunMeta({
|
|
9420
|
+
runId,
|
|
9421
|
+
source: "plain",
|
|
9422
|
+
request: task,
|
|
9423
|
+
contract,
|
|
9424
|
+
repoScaleMode: indexInfo.scaleMode,
|
|
9425
|
+
semanticStatus,
|
|
9426
|
+
maxParallel,
|
|
9427
|
+
budgetUsd: options.budgetUsd,
|
|
9428
|
+
});
|
|
9429
|
+
await this.executeSwarmRun({
|
|
9430
|
+
runId,
|
|
9431
|
+
plan,
|
|
9432
|
+
meta,
|
|
9433
|
+
contract,
|
|
9434
|
+
budgetUsd: options.budgetUsd,
|
|
9435
|
+
projectIndex: indexInfo.index,
|
|
9436
|
+
enableIncrementalIndex: indexInfo.scaleMode !== "small",
|
|
9437
|
+
});
|
|
9438
|
+
}
|
|
9439
|
+
async runSwarmFromSingular(input) {
|
|
9440
|
+
const analysis = this.loadSingularAnalysisByRunId(input.runId);
|
|
9441
|
+
if (!analysis) {
|
|
9442
|
+
this.showWarning(`Singular run not found: ${input.runId}`);
|
|
9443
|
+
return;
|
|
9444
|
+
}
|
|
9445
|
+
const option = analysis.options.find((item) => item.id === String(input.option));
|
|
9446
|
+
if (!option) {
|
|
9447
|
+
this.showWarning(`Option ${input.option} not found in singular run ${input.runId}.`);
|
|
9448
|
+
return;
|
|
9449
|
+
}
|
|
9450
|
+
const contract = await this.ensureSwarmEffectiveContract(analysis.request);
|
|
9451
|
+
if (!contract)
|
|
9452
|
+
return;
|
|
9453
|
+
const indexInfo = this.ensureSwarmProjectIndex(`${analysis.request} ${option.title}`);
|
|
9454
|
+
const semanticStatus = await this.maybeWarnSwarmSemantic(indexInfo.scaleMode);
|
|
9455
|
+
const plan = buildSwarmPlanFromSingular({
|
|
9456
|
+
analysis,
|
|
9457
|
+
option,
|
|
9458
|
+
contract,
|
|
9459
|
+
index: indexInfo.index,
|
|
9460
|
+
});
|
|
9461
|
+
const runId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
9462
|
+
const maxParallel = Math.max(1, Math.min(20, input.maxParallel ?? 3));
|
|
9463
|
+
const meta = this.buildSwarmRunMeta({
|
|
9464
|
+
runId,
|
|
9465
|
+
source: "singular",
|
|
9466
|
+
request: analysis.request,
|
|
9467
|
+
contract,
|
|
9468
|
+
repoScaleMode: indexInfo.scaleMode,
|
|
9469
|
+
semanticStatus,
|
|
9470
|
+
maxParallel,
|
|
9471
|
+
budgetUsd: input.budgetUsd,
|
|
9472
|
+
linkedSingularRunId: analysis.runId,
|
|
9473
|
+
linkedSingularOption: option.id,
|
|
9474
|
+
});
|
|
9475
|
+
await this.executeSwarmRun({
|
|
9476
|
+
runId,
|
|
9477
|
+
plan,
|
|
9478
|
+
meta,
|
|
9479
|
+
contract,
|
|
9480
|
+
budgetUsd: input.budgetUsd,
|
|
9481
|
+
projectIndex: indexInfo.index,
|
|
9482
|
+
enableIncrementalIndex: indexInfo.scaleMode !== "small",
|
|
9483
|
+
});
|
|
9484
|
+
}
|
|
9485
|
+
loadSwarmRunBundle(runId) {
|
|
9486
|
+
const store = new SwarmStateStore(this.sessionManager.getCwd(), runId);
|
|
9487
|
+
const meta = store.loadMeta();
|
|
9488
|
+
const plan = store.loadPlan();
|
|
9489
|
+
const state = store.loadState();
|
|
9490
|
+
if (!meta || !plan || !state)
|
|
9491
|
+
return undefined;
|
|
9492
|
+
return { meta, plan, state };
|
|
9493
|
+
}
|
|
9494
|
+
computeSwarmCriticalPathLength(plan) {
|
|
9495
|
+
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
9496
|
+
const memo = new Map();
|
|
9497
|
+
const visiting = new Set();
|
|
9498
|
+
const visit = (taskId) => {
|
|
9499
|
+
if (memo.has(taskId))
|
|
9500
|
+
return memo.get(taskId);
|
|
9501
|
+
if (visiting.has(taskId))
|
|
9502
|
+
return 1;
|
|
9503
|
+
visiting.add(taskId);
|
|
9504
|
+
const task = byId.get(taskId);
|
|
9505
|
+
if (!task)
|
|
9506
|
+
return 1;
|
|
9507
|
+
const depMax = task.depends_on.reduce((maxValue, depId) => Math.max(maxValue, visit(depId)), 0);
|
|
9508
|
+
const value = depMax + 1;
|
|
9509
|
+
memo.set(taskId, value);
|
|
9510
|
+
visiting.delete(taskId);
|
|
9511
|
+
return value;
|
|
9512
|
+
};
|
|
9513
|
+
let best = 0;
|
|
9514
|
+
for (const task of plan.tasks) {
|
|
9515
|
+
best = Math.max(best, visit(task.id));
|
|
9516
|
+
}
|
|
9517
|
+
return best;
|
|
9518
|
+
}
|
|
9519
|
+
computeSwarmDependentCounts(plan) {
|
|
9520
|
+
const counts = new Map();
|
|
9521
|
+
for (const task of plan.tasks) {
|
|
9522
|
+
for (const dep of task.depends_on) {
|
|
9523
|
+
counts.set(dep, (counts.get(dep) ?? 0) + 1);
|
|
9524
|
+
}
|
|
9525
|
+
}
|
|
9526
|
+
return counts;
|
|
9527
|
+
}
|
|
9528
|
+
formatSwarmWatch(meta, plan, state) {
|
|
9529
|
+
const tasks = Object.values(state.tasks);
|
|
9530
|
+
const done = tasks.filter((task) => task.status === "done").length;
|
|
9531
|
+
const running = tasks.filter((task) => task.status === "running").length;
|
|
9532
|
+
const blocked = tasks.filter((task) => task.status === "blocked").length;
|
|
9533
|
+
const errors = tasks.filter((task) => task.status === "error").length;
|
|
9534
|
+
const pending = tasks.filter((task) => task.status === "pending" || task.status === "ready").length;
|
|
9535
|
+
const total = tasks.length;
|
|
9536
|
+
const remaining = Math.max(0, total - done);
|
|
9537
|
+
const throughputPerTick = done > 0 && state.tick > 0 ? done / state.tick : 0;
|
|
9538
|
+
const etaTicks = throughputPerTick > 0 ? Math.ceil(remaining / throughputPerTick) : undefined;
|
|
9539
|
+
const criticalPath = this.computeSwarmCriticalPathLength(plan);
|
|
9540
|
+
const speedupPotential = criticalPath > 0 ? total / criticalPath : 1;
|
|
9541
|
+
const dependentCounts = this.computeSwarmDependentCounts(plan);
|
|
9542
|
+
const bottlenecks = [...dependentCounts.entries()]
|
|
9543
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
9544
|
+
.slice(0, 3);
|
|
9545
|
+
const lines = [
|
|
9546
|
+
`run_id: ${meta.runId}`,
|
|
9547
|
+
`status: ${state.status}`,
|
|
9548
|
+
`source: ${meta.source}`,
|
|
9549
|
+
`request: ${meta.request}`,
|
|
9550
|
+
"consistency_model: Scopes -> Touches -> Locks -> Gates -> Done",
|
|
9551
|
+
`progress: done=${done}/${tasks.length} running=${running} pending=${pending} blocked=${blocked} error=${errors}`,
|
|
9552
|
+
`budget_usd: ${state.budget.spentUsd.toFixed(2)}${meta.budgetUsd ? `/${meta.budgetUsd.toFixed(2)}` : ""} warned80=${state.budget.warned80 ? "yes" : "no"}`,
|
|
9553
|
+
`tick: ${state.tick} no_progress_ticks: ${state.noProgressTicks}`,
|
|
9554
|
+
`eta_ticks: ${etaTicks ?? "unknown"} throughput_per_tick=${throughputPerTick > 0 ? throughputPerTick.toFixed(2) : "0.00"}`,
|
|
9555
|
+
`critical_path: ${criticalPath} theoretical_speedup=${speedupPotential.toFixed(2)}x`,
|
|
9556
|
+
`repo_scale: ${meta.repoScaleMode} semantic: ${meta.semanticStatus ?? "unknown"}`,
|
|
9557
|
+
"",
|
|
9558
|
+
...(bottlenecks.length > 0
|
|
9559
|
+
? ["bottlenecks:", ...bottlenecks.map(([taskId, dependents]) => `- ${taskId}: unlocks ${dependents} downstream task(s)`), ""]
|
|
9560
|
+
: []),
|
|
9561
|
+
"tasks:",
|
|
9562
|
+
...plan.tasks.map((task) => {
|
|
9563
|
+
const runtime = state.tasks[task.id];
|
|
9564
|
+
if (!runtime)
|
|
9565
|
+
return `- ${task.id}: missing runtime state`;
|
|
9566
|
+
return `- ${task.id}: ${runtime.status} attempts=${runtime.attempts} touches=${runtime.touches.slice(0, 3).join(",") || "-"}`;
|
|
9567
|
+
}),
|
|
9568
|
+
];
|
|
9569
|
+
if (Object.keys(state.locks).length > 0) {
|
|
9570
|
+
lines.push("", "locks:");
|
|
9571
|
+
for (const [taskId, touches] of Object.entries(state.locks)) {
|
|
9572
|
+
lines.push(`- ${taskId}: ${touches.join(", ")}`);
|
|
9573
|
+
}
|
|
9574
|
+
}
|
|
9575
|
+
return lines.join("\n");
|
|
9576
|
+
}
|
|
9577
|
+
async runSwarmResume(runId) {
|
|
9578
|
+
const bundle = this.loadSwarmRunBundle(runId);
|
|
9579
|
+
if (!bundle) {
|
|
9580
|
+
this.showWarning(`Swarm run not found or incomplete: ${runId}`);
|
|
9581
|
+
return;
|
|
9582
|
+
}
|
|
9583
|
+
if (bundle.state.status === "completed") {
|
|
9584
|
+
this.showStatus(`Swarm run ${runId} is already completed.`);
|
|
9585
|
+
return;
|
|
9586
|
+
}
|
|
9587
|
+
bundle.state.status = "running";
|
|
9588
|
+
bundle.state.updatedAt = new Date().toISOString();
|
|
9589
|
+
await this.executeSwarmRun({
|
|
9590
|
+
runId,
|
|
9591
|
+
plan: bundle.plan,
|
|
9592
|
+
meta: bundle.meta,
|
|
9593
|
+
contract: bundle.meta.contract,
|
|
9594
|
+
budgetUsd: bundle.meta.budgetUsd,
|
|
9595
|
+
resumeState: bundle.state,
|
|
9596
|
+
projectIndex: bundle.meta.repoScaleMode === "small" ? undefined : loadProjectIndex(this.sessionManager.getCwd()),
|
|
9597
|
+
enableIncrementalIndex: bundle.meta.repoScaleMode !== "small",
|
|
9598
|
+
});
|
|
9599
|
+
}
|
|
9600
|
+
async runSwarmRetry(runId, taskId, resetBrief) {
|
|
9601
|
+
const bundle = this.loadSwarmRunBundle(runId);
|
|
9602
|
+
if (!bundle) {
|
|
9603
|
+
this.showWarning(`Swarm run not found or incomplete: ${runId}`);
|
|
9604
|
+
return;
|
|
9605
|
+
}
|
|
9606
|
+
const target = bundle.state.tasks[taskId];
|
|
9607
|
+
if (!target) {
|
|
9608
|
+
this.showWarning(`Task ${taskId} not found in run ${runId}.`);
|
|
9609
|
+
return;
|
|
9610
|
+
}
|
|
9611
|
+
if (resetBrief) {
|
|
9612
|
+
const existingPlan = bundle.plan.tasks.find((task) => task.id === taskId);
|
|
9613
|
+
const edited = await this.showExtensionInput(`/swarm retry: update brief for ${taskId}`, existingPlan?.brief ?? target.id);
|
|
9614
|
+
if (edited === undefined) {
|
|
9615
|
+
this.showStatus("Swarm retry cancelled.");
|
|
9616
|
+
return;
|
|
9617
|
+
}
|
|
9618
|
+
if (existingPlan) {
|
|
9619
|
+
existingPlan.brief = edited.trim() || existingPlan.brief;
|
|
9620
|
+
}
|
|
9621
|
+
}
|
|
9622
|
+
target.status = "ready";
|
|
9623
|
+
target.lastError = undefined;
|
|
9624
|
+
target.completedAt = undefined;
|
|
9625
|
+
bundle.state.retries[taskId] = 0;
|
|
9626
|
+
bundle.state.status = "running";
|
|
9627
|
+
bundle.state.updatedAt = new Date().toISOString();
|
|
9628
|
+
const store = new SwarmStateStore(this.sessionManager.getCwd(), runId);
|
|
9629
|
+
store.savePlan(bundle.plan);
|
|
9630
|
+
store.saveState(bundle.state);
|
|
9631
|
+
store.appendEvent({
|
|
9632
|
+
type: "task_retry",
|
|
9633
|
+
timestamp: new Date().toISOString(),
|
|
9634
|
+
runId,
|
|
9635
|
+
taskId,
|
|
9636
|
+
message: resetBrief ? "manual retry with reset brief" : "manual retry",
|
|
9637
|
+
});
|
|
9638
|
+
await this.executeSwarmRun({
|
|
9639
|
+
runId,
|
|
9640
|
+
plan: bundle.plan,
|
|
9641
|
+
meta: bundle.meta,
|
|
9642
|
+
contract: bundle.meta.contract,
|
|
9643
|
+
budgetUsd: bundle.meta.budgetUsd,
|
|
9644
|
+
resumeState: bundle.state,
|
|
9645
|
+
projectIndex: bundle.meta.repoScaleMode === "small" ? undefined : loadProjectIndex(this.sessionManager.getCwd()),
|
|
9646
|
+
enableIncrementalIndex: bundle.meta.repoScaleMode !== "small",
|
|
9647
|
+
});
|
|
9648
|
+
}
|
|
9649
|
+
async handleSwarmCommand(text) {
|
|
9650
|
+
if (this.session.isStreaming) {
|
|
9651
|
+
this.showWarning("Cannot run /swarm while the agent is processing another request.");
|
|
9652
|
+
return;
|
|
9653
|
+
}
|
|
9654
|
+
if (this.session.isCompacting) {
|
|
9655
|
+
this.showWarning("Cannot run /swarm while compaction is running.");
|
|
9656
|
+
return;
|
|
9657
|
+
}
|
|
9658
|
+
const parsed = this.parseSwarmCommand(text);
|
|
9659
|
+
if (!parsed)
|
|
9660
|
+
return;
|
|
9661
|
+
if (parsed.subcommand === "help") {
|
|
9662
|
+
this.showCommandTextBlock("Swarm Help", this.getSwarmHelpText());
|
|
9663
|
+
return;
|
|
9664
|
+
}
|
|
9665
|
+
if (parsed.subcommand === "watch") {
|
|
9666
|
+
let runId = parsed.runId;
|
|
9667
|
+
if (!runId) {
|
|
9668
|
+
const runs = SwarmStateStore.listRuns(this.sessionManager.getCwd(), 20);
|
|
9669
|
+
if (runs.length === 0) {
|
|
9670
|
+
this.showStatus("No swarm runs found.");
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
runId = runs[0].runId;
|
|
9674
|
+
}
|
|
9675
|
+
const bundle = this.loadSwarmRunBundle(runId);
|
|
9676
|
+
if (!bundle) {
|
|
9677
|
+
this.showWarning(`Swarm run not found or incomplete: ${runId}`);
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9680
|
+
this.showCommandTextBlock("Swarm Watch", this.formatSwarmWatch(bundle.meta, bundle.plan, bundle.state));
|
|
9681
|
+
return;
|
|
9682
|
+
}
|
|
9683
|
+
if (parsed.subcommand === "resume") {
|
|
9684
|
+
await this.runSwarmResume(parsed.runId);
|
|
9685
|
+
return;
|
|
9686
|
+
}
|
|
9687
|
+
if (parsed.subcommand === "retry") {
|
|
9688
|
+
await this.runSwarmRetry(parsed.runId, parsed.taskId, parsed.resetBrief);
|
|
9689
|
+
return;
|
|
9690
|
+
}
|
|
9691
|
+
if (parsed.subcommand === "run") {
|
|
9692
|
+
await this.runSwarmFromTask(parsed.task, {
|
|
9693
|
+
maxParallel: parsed.maxParallel,
|
|
9694
|
+
budgetUsd: parsed.budgetUsd,
|
|
9695
|
+
});
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
if (parsed.subcommand === "from-singular") {
|
|
9699
|
+
await this.runSwarmFromSingular({
|
|
9700
|
+
runId: parsed.runId,
|
|
9701
|
+
option: parsed.option,
|
|
9702
|
+
maxParallel: parsed.maxParallel,
|
|
9703
|
+
budgetUsd: parsed.budgetUsd,
|
|
9704
|
+
});
|
|
9705
|
+
}
|
|
9706
|
+
}
|
|
8388
9707
|
parseOrchestrateSlashCommand(text) {
|
|
8389
9708
|
const args = this.parseSlashArgs(text).slice(1);
|
|
8390
9709
|
let mode;
|
|
@@ -8590,10 +9909,20 @@ export class InteractiveMode {
|
|
|
8590
9909
|
this.showWarning("Cannot run /orchestrate while compaction is running.");
|
|
8591
9910
|
return;
|
|
8592
9911
|
}
|
|
9912
|
+
const rawArgs = this.parseSlashArgs(text).slice(1);
|
|
9913
|
+
if (rawArgs.includes("--swarm")) {
|
|
9914
|
+
this.showWarning("`/orchestrate --swarm` was removed to avoid ambiguity. Use `/swarm` commands directly.");
|
|
9915
|
+
this.showCommandTextBlock("Swarm Usage", this.getSwarmHelpText());
|
|
9916
|
+
return;
|
|
9917
|
+
}
|
|
8593
9918
|
const parsed = this.parseOrchestrateSlashCommand(text);
|
|
8594
9919
|
if (!parsed) {
|
|
8595
9920
|
return;
|
|
8596
9921
|
}
|
|
9922
|
+
const swarmRecommendation = this.buildSwarmRecommendationFromOrchestrate(parsed);
|
|
9923
|
+
if (swarmRecommendation.recommend) {
|
|
9924
|
+
this.showWarning(`This task looks complex/risky for legacy /orchestrate (${swarmRecommendation.reasons.join("; ")}). Consider ${swarmRecommendation.command}.`);
|
|
9925
|
+
}
|
|
8597
9926
|
const currentCwd = this.sessionManager.getCwd();
|
|
8598
9927
|
const assignments = [];
|
|
8599
9928
|
const assignmentRecords = [];
|
|
@@ -8637,6 +9966,7 @@ export class InteractiveMode {
|
|
|
8637
9966
|
"- use task tool for every agent assignment",
|
|
8638
9967
|
"- for parallel mode, emit all independent task calls in one assistant response",
|
|
8639
9968
|
"- in parallel mode, use parallel tool-call style (<use_parallel_tool_calls>)",
|
|
9969
|
+
"- when assignment lines include depends_on, still emit one task call per assignment; runtime enforces dependency gating",
|
|
8640
9970
|
"- keep required orchestration task calls in foreground; do not set background=true unless user explicitly requested detached async runs",
|
|
8641
9971
|
"- do not poll .iosm/subagents/background via bash/read during orchestration; wait for task results and then synthesize",
|
|
8642
9972
|
"- include run_id and task_id from each assignment in the task tool arguments",
|
|
@@ -10746,6 +12076,7 @@ The agent will automatically receive IOSM context on every turn.`;
|
|
|
10746
12076
|
return result;
|
|
10747
12077
|
}
|
|
10748
12078
|
stop() {
|
|
12079
|
+
this.clearSubagentElapsedTimer();
|
|
10749
12080
|
if (this.loadingAnimation) {
|
|
10750
12081
|
this.loadingAnimation.stop();
|
|
10751
12082
|
this.loadingAnimation = undefined;
|